feat(audio, ui): add studio audio mixer, soft limiter, and revamped cyber-slate overlay
This commit is contained in:
parent
763cc04ad1
commit
524f771cbf
6 changed files with 444 additions and 131 deletions
|
|
@ -125,10 +125,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
let (cmd_tx, cmd_rx) = bounded::<Command>(10);
|
||||
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
|
||||
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(500);
|
||||
let audio_capture_initial = vrec::capture::audio::AudioCapture::new_with_device_and_mode(
|
||||
let audio_capture_initial = vrec::capture::audio::AudioCapture::new_with_device_mode_and_volumes(
|
||||
audio_tx.clone(),
|
||||
Some(&initial_config.audio_device),
|
||||
&initial_config.audio_mode,
|
||||
initial_config.mic_volume,
|
||||
initial_config.system_volume,
|
||||
).ok();
|
||||
|
||||
// Automatically register dynamic keybindings on Hyprland if active (no config edits needed)
|
||||
|
|
@ -301,13 +303,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
println!("Replay buffer resized to {} packets.", new_capacity);
|
||||
}
|
||||
vrec::hyprland_binds::register_hyprland_binds(&config);
|
||||
audio_capture = match vrec::capture::audio::AudioCapture::new_with_device_and_mode(
|
||||
audio_capture = match vrec::capture::audio::AudioCapture::new_with_device_mode_and_volumes(
|
||||
audio_tx_clone.clone(),
|
||||
Some(&config.audio_device),
|
||||
&config.audio_mode,
|
||||
config.mic_volume,
|
||||
config.system_volume,
|
||||
) {
|
||||
Ok((c, _, _)) => {
|
||||
println!("Audio capture reloaded for mode: {}", config.audio_mode);
|
||||
println!("Audio capture reloaded for mode: {} (mic: {:.0}%, sys: {:.0}%)", config.audio_mode, config.mic_volume * 100.0, config.system_volume * 100.0);
|
||||
Some(c)
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -512,6 +516,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
audio_muted: audio_muted_state.load(Ordering::SeqCst),
|
||||
audio_mode: cfg.audio_mode,
|
||||
show_cursor: cfg.show_cursor,
|
||||
mic_volume: cfg.mic_volume,
|
||||
system_volume: cfg.system_volume,
|
||||
};
|
||||
if let Ok(resp) = serde_json::to_vec(&status) {
|
||||
let len_resp = (resp.len() as u32).to_le_bytes();
|
||||
|
|
|
|||
|
|
@ -136,6 +136,24 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
"--reload" => {
|
||||
return send_command(Command::ReloadConfig);
|
||||
}
|
||||
"--status" => {
|
||||
match query_status() {
|
||||
Ok(st) => {
|
||||
println!("Daemon Status:");
|
||||
println!(" State: {}", if st.is_recording { format!("RECORDING ({}s)", st.recording_duration_sec) } else { "IDLE".to_string() });
|
||||
println!(" Instant Replay: {}", if st.is_replay_active { "ACTIVE" } else { "OFF" });
|
||||
println!(" Audio Mode: {}", st.audio_mode);
|
||||
println!(" Mic Volume: {:.0}%", st.mic_volume * 100.0);
|
||||
println!(" System Volume: {:.0}%", st.system_volume * 100.0);
|
||||
println!(" Show Cursor: {}", st.show_cursor);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to vrec daemon: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"--quit" => {
|
||||
return send_command(Command::StopDaemon);
|
||||
}
|
||||
|
|
@ -144,6 +162,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
println!("Usage:");
|
||||
println!(" vrec-ui Run global hotkey manager in background");
|
||||
println!(" vrec-ui --menu Open overlay menu");
|
||||
println!(" vrec-ui --status Query current daemon status");
|
||||
println!(" vrec-ui --save Save instant replay and show notification");
|
||||
println!(" vrec-ui --record Toggle normal recording on/off (with notification)");
|
||||
println!(" vrec-ui --cursor Toggle mouse cursor recording on/off (with notification)");
|
||||
|
|
|
|||
|
|
@ -54,6 +54,17 @@ pub fn list_input_devices() -> Vec<String> {
|
|||
names
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn soft_limit(x: f32) -> f32 {
|
||||
if x.abs() <= 0.75 {
|
||||
x
|
||||
} else if x > 0.0 {
|
||||
0.75 + 0.24 * ((x - 0.75) / 0.24).tanh()
|
||||
} else {
|
||||
-0.75 - 0.24 * ((-x - 0.75) / 0.24).tanh()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_parec_available() -> bool {
|
||||
std::process::Command::new("parec")
|
||||
|
|
@ -69,6 +80,7 @@ fn is_parec_available() -> bool {
|
|||
fn spawn_parec_stream(
|
||||
device: &str,
|
||||
sender: Sender<Vec<f32>>,
|
||||
gain: f32,
|
||||
) -> Result<std::process::Child, Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::io::Read;
|
||||
|
||||
|
|
@ -113,7 +125,8 @@ fn spawn_parec_stream(
|
|||
combined[i * 4 + 2],
|
||||
combined[i * 4 + 3],
|
||||
];
|
||||
floats.push(f32::from_le_bytes(b));
|
||||
let raw = f32::from_le_bytes(b);
|
||||
floats.push(soft_limit(raw * gain));
|
||||
}
|
||||
|
||||
remainder.clear();
|
||||
|
|
@ -133,20 +146,30 @@ fn spawn_parec_stream(
|
|||
|
||||
impl AudioCapture {
|
||||
pub fn new(sender: Sender<Vec<f32>>) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::new_with_device_and_mode(sender, None, "system")
|
||||
Self::new_with_device_mode_and_volumes(sender, None, "system", 0.60, 1.0)
|
||||
}
|
||||
|
||||
pub fn new_with_device(
|
||||
sender: Sender<Vec<f32>>,
|
||||
device_name: Option<&str>,
|
||||
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::new_with_device_and_mode(sender, device_name, "system")
|
||||
Self::new_with_device_mode_and_volumes(sender, device_name, "system", 0.60, 1.0)
|
||||
}
|
||||
|
||||
pub fn new_with_device_and_mode(
|
||||
sender: Sender<Vec<f32>>,
|
||||
device_name: Option<&str>,
|
||||
audio_mode: &str,
|
||||
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Self::new_with_device_mode_and_volumes(sender, device_name, audio_mode, 0.60, 1.0)
|
||||
}
|
||||
|
||||
pub fn new_with_device_mode_and_volumes(
|
||||
sender: Sender<Vec<f32>>,
|
||||
device_name: Option<&str>,
|
||||
audio_mode: &str,
|
||||
mic_volume: f32,
|
||||
system_volume: f32,
|
||||
) -> Result<(Self, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if audio_mode == "muted" {
|
||||
println!("Audio capture mode: MUTED (No audio recorded)");
|
||||
|
|
@ -165,8 +188,8 @@ impl AudioCapture {
|
|||
let mic_target = device_name
|
||||
.filter(|d| *d != "default" && !d.trim().is_empty())
|
||||
.unwrap_or("@DEFAULT_SOURCE@");
|
||||
let child = spawn_parec_stream(mic_target, sender)?;
|
||||
println!("Audio capture mode: MICROPHONE ONLY [{}] (48000 Hz, 2 ch)", mic_target);
|
||||
let child = spawn_parec_stream(mic_target, sender, mic_volume)?;
|
||||
println!("Audio capture mode: MICROPHONE ONLY [{}] (vol: {:.0}%, 48000 Hz, 2 ch)", mic_target, mic_volume * 100.0);
|
||||
return Ok((Self {
|
||||
_processes: vec![child],
|
||||
_streams: Vec::new(),
|
||||
|
|
@ -176,11 +199,11 @@ impl AudioCapture {
|
|||
let (sys_tx, sys_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
let (mic_tx, mic_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
|
||||
let sys_child = spawn_parec_stream("@DEFAULT_MONITOR@", sys_tx)?;
|
||||
let sys_child = spawn_parec_stream("@DEFAULT_MONITOR@", sys_tx, system_volume)?;
|
||||
let mic_target = device_name
|
||||
.filter(|d| *d != "default" && !d.trim().is_empty())
|
||||
.unwrap_or("@DEFAULT_SOURCE@");
|
||||
let mic_child = spawn_parec_stream(mic_target, mic_tx)?;
|
||||
let mic_child = spawn_parec_stream(mic_target, mic_tx, mic_volume)?;
|
||||
|
||||
let out_tx = sender;
|
||||
thread::spawn(move || {
|
||||
|
|
@ -201,7 +224,7 @@ impl AudioCapture {
|
|||
for _ in 0..avail {
|
||||
let s = sys_q.pop_front().unwrap_or(0.0);
|
||||
let m = mic_q.pop_front().unwrap_or(0.0);
|
||||
mixed.push((s + m * 0.9).clamp(-1.0, 1.0));
|
||||
mixed.push(soft_limit(s + m));
|
||||
}
|
||||
let _ = out_tx.try_send(mixed);
|
||||
} else if sys_q.len() > 4800 {
|
||||
|
|
@ -216,7 +239,7 @@ impl AudioCapture {
|
|||
}
|
||||
});
|
||||
|
||||
println!("Audio capture mode: BOTH (System [@DEFAULT_MONITOR@] + Mic [{}]) (48000 Hz, 2 ch)", mic_target);
|
||||
println!("Audio capture mode: BOTH (System [@DEFAULT_MONITOR@, {:.0}%] + Mic [{}, {:.0}%]) (48000 Hz, 2 ch)", system_volume * 100.0, mic_target, mic_volume * 100.0);
|
||||
return Ok((Self {
|
||||
_processes: vec![sys_child, mic_child],
|
||||
_streams: Vec::new(),
|
||||
|
|
@ -224,8 +247,8 @@ impl AudioCapture {
|
|||
}
|
||||
_ => {
|
||||
// System audio only
|
||||
let child = spawn_parec_stream("@DEFAULT_MONITOR@", sender)?;
|
||||
println!("Audio capture mode: SYSTEM AUDIO ONLY [@DEFAULT_MONITOR@] (48000 Hz, 2 ch)");
|
||||
let child = spawn_parec_stream("@DEFAULT_MONITOR@", sender, system_volume)?;
|
||||
println!("Audio capture mode: SYSTEM AUDIO ONLY [@DEFAULT_MONITOR@] (vol: {:.0}%, 48000 Hz, 2 ch)", system_volume * 100.0);
|
||||
return Ok((Self {
|
||||
_processes: vec![child],
|
||||
_streams: Vec::new(),
|
||||
|
|
@ -272,7 +295,7 @@ impl AudioCapture {
|
|||
host.default_input_device()
|
||||
};
|
||||
|
||||
let build_stream = |dev: &cpal::Device, tx: Sender<Vec<f32>>| -> Result<(cpal::Stream, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let build_stream = |dev: &cpal::Device, tx: Sender<Vec<f32>>, gain: f32| -> Result<(cpal::Stream, u32, u16), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let config = dev.default_input_config()?;
|
||||
let sample_rate = config.sample_rate();
|
||||
let channels = config.channels();
|
||||
|
|
@ -285,7 +308,8 @@ impl AudioCapture {
|
|||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[f32], _: &_| {
|
||||
let _ = tx.try_send(data.to_vec());
|
||||
let f32_data: Vec<f32> = data.iter().map(|&s| soft_limit(s * gain)).collect();
|
||||
let _ = tx.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
|
|
@ -295,7 +319,7 @@ impl AudioCapture {
|
|||
dev.build_input_stream(
|
||||
stream_config,
|
||||
move |data: &[i16], _: &_| {
|
||||
let f32_data: Vec<f32> = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
|
||||
let f32_data: Vec<f32> = data.iter().map(|&s| soft_limit((s as f32 / i16::MAX as f32) * gain)).collect();
|
||||
let _ = tx.try_send(f32_data);
|
||||
},
|
||||
err_fn,
|
||||
|
|
@ -312,7 +336,7 @@ impl AudioCapture {
|
|||
"mic" => {
|
||||
let dev = find_mic_device().ok_or("No microphone device found")?;
|
||||
let name = get_device_name(&dev);
|
||||
let (stream, sr, ch) = build_stream(&dev, sender)?;
|
||||
let (stream, sr, ch) = build_stream(&dev, sender, mic_volume)?;
|
||||
println!("Audio capture mode: MICROPHONE ONLY [{}] ({} Hz, {} ch)", name, sr, ch);
|
||||
Ok((Self {
|
||||
#[cfg(unix)]
|
||||
|
|
@ -330,8 +354,8 @@ impl AudioCapture {
|
|||
let (sys_tx, sys_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
let (mic_tx, mic_rx) = crossbeam_channel::bounded::<Vec<f32>>(100);
|
||||
|
||||
let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx)?;
|
||||
let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx)?;
|
||||
let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx, system_volume)?;
|
||||
let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx, mic_volume)?;
|
||||
|
||||
let out_tx = sender;
|
||||
thread::spawn(move || {
|
||||
|
|
@ -352,7 +376,7 @@ impl AudioCapture {
|
|||
for _ in 0..avail {
|
||||
let s = sys_q.pop_front().unwrap_or(0.0);
|
||||
let m = mic_q.pop_front().unwrap_or(0.0);
|
||||
mixed.push((s + m * 0.9).clamp(-1.0, 1.0));
|
||||
mixed.push(soft_limit(s + m));
|
||||
}
|
||||
let _ = out_tx.try_send(mixed);
|
||||
} else if sys_q.len() > 4800 {
|
||||
|
|
@ -377,7 +401,7 @@ impl AudioCapture {
|
|||
_ => {
|
||||
let dev = find_system_device().ok_or("No audio device available")?;
|
||||
let name = get_device_name(&dev);
|
||||
let (stream, sr, ch) = build_stream(&dev, sender)?;
|
||||
let (stream, sr, ch) = build_stream(&dev, sender, system_volume)?;
|
||||
println!("Audio capture mode: SYSTEM SOUNDS ONLY [{}] ({} Hz, {} ch)", name, sr, ch);
|
||||
Ok((Self {
|
||||
#[cfg(unix)]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ fn default_save_hotkey() -> String { "Ctrl+Shift+R".to_string() }
|
|||
fn default_menu_hotkey() -> String { "Alt+Z".to_string() }
|
||||
fn default_record_hotkey() -> String { "Ctrl+Shift+F9".to_string() }
|
||||
fn default_cursor_hotkey() -> String { "Ctrl+Shift+F10".to_string() }
|
||||
fn default_mic_volume() -> f32 { 0.60 }
|
||||
fn default_system_volume() -> f32 { 1.00 }
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct VrecConfig {
|
||||
|
|
@ -42,6 +44,10 @@ pub struct VrecConfig {
|
|||
pub audio_device: String,
|
||||
#[serde(default = "default_audio_mode")]
|
||||
pub audio_mode: String,
|
||||
#[serde(default = "default_mic_volume")]
|
||||
pub mic_volume: f32,
|
||||
#[serde(default = "default_system_volume")]
|
||||
pub system_volume: f32,
|
||||
|
||||
#[serde(default)]
|
||||
pub autostart: bool,
|
||||
|
|
@ -74,6 +80,8 @@ impl Default for VrecConfig {
|
|||
show_cursor: true,
|
||||
audio_device: "default".to_string(),
|
||||
audio_mode: "system".to_string(),
|
||||
mic_volume: default_mic_volume(),
|
||||
system_volume: default_system_volume(),
|
||||
|
||||
autostart: false,
|
||||
language: "en".to_string(),
|
||||
|
|
@ -151,5 +159,7 @@ mod tests {
|
|||
assert_eq!(parsed.replay_duration_sec, 60);
|
||||
assert_eq!(parsed.audio_mode, "system");
|
||||
assert!(parsed.show_cursor);
|
||||
assert!((parsed.mic_volume - 0.60).abs() < 1e-4);
|
||||
assert!((parsed.system_volume - 1.00).abs() < 1e-4);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
src/ipc.rs
12
src/ipc.rs
|
|
@ -25,12 +25,24 @@ pub struct DaemonStatus {
|
|||
pub audio_mode: String,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_cursor: bool,
|
||||
#[serde(default = "default_mic_volume")]
|
||||
pub mic_volume: f32,
|
||||
#[serde(default = "default_system_volume")]
|
||||
pub system_volume: f32,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_mic_volume() -> f32 {
|
||||
0.60
|
||||
}
|
||||
|
||||
fn default_system_volume() -> f32 {
|
||||
1.00
|
||||
}
|
||||
|
||||
fn default_audio_mode_str() -> String {
|
||||
"system".to_string()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::ipc::{self, Command, DaemonStatus};
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OverlayTab {
|
||||
Dashboard,
|
||||
AudioMixer,
|
||||
Settings,
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +23,9 @@ pub struct VrecOverlayApp {
|
|||
target_fps: u32,
|
||||
audio_mode_idx: usize,
|
||||
show_cursor: bool,
|
||||
mic_volume_pct: u32,
|
||||
system_volume_pct: u32,
|
||||
anim_time: f32,
|
||||
status_msg: Option<(String, Instant)>,
|
||||
status_rx: Receiver<DaemonStatus>,
|
||||
folder_tx: Sender<String>,
|
||||
|
|
@ -36,6 +40,8 @@ impl VrecOverlayApp {
|
|||
let target_fps = config.fps;
|
||||
let output_dir = config.output_directory.clone();
|
||||
let show_cursor = config.show_cursor;
|
||||
let mic_volume_pct = (config.mic_volume * 100.0).round().clamp(0.0, 200.0) as u32;
|
||||
let system_volume_pct = (config.system_volume * 100.0).round().clamp(0.0, 200.0) as u32;
|
||||
let audio_mode_idx = match config.audio_mode.as_str() {
|
||||
"mic" => 1,
|
||||
"both" => 2,
|
||||
|
|
@ -50,7 +56,7 @@ impl VrecOverlayApp {
|
|||
if let Ok(s) = ipc::query_status() {
|
||||
let _ = status_tx.send(s);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -68,6 +74,9 @@ impl VrecOverlayApp {
|
|||
target_fps,
|
||||
audio_mode_idx,
|
||||
show_cursor,
|
||||
mic_volume_pct,
|
||||
system_volume_pct,
|
||||
anim_time: 0.0,
|
||||
status_msg: None,
|
||||
status_rx,
|
||||
folder_tx,
|
||||
|
|
@ -102,23 +111,75 @@ impl VrecOverlayApp {
|
|||
// Helper to render realistic mechanical keyboard keycap badges
|
||||
fn render_keycap(ui: &mut egui::Ui, text: &str) {
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(20, 25, 36))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(46, 54, 72)))
|
||||
.corner_radius(CornerRadius::same(4_u8))
|
||||
.inner_margin(Margin::symmetric(7_i8, 3_i8))
|
||||
.fill(Color32::from_rgb(18, 24, 38))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(45, 58, 82)))
|
||||
.corner_radius(CornerRadius::same(5_u8))
|
||||
.inner_margin(Margin::symmetric(8_i8, 3_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(text)
|
||||
.font(FontId::monospace(10.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(148, 163, 184)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper pill button
|
||||
fn pill(ui: &mut egui::Ui, text: &str, active: bool) -> bool {
|
||||
let fill = if active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(22, 28, 40) };
|
||||
let stroke = if active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(42, 53, 75)) };
|
||||
let btn = egui::Button::new(egui::RichText::new(text).size(11.0).color(Color32::from_rgb(240, 246, 252)))
|
||||
.fill(fill)
|
||||
.stroke(stroke)
|
||||
.corner_radius(CornerRadius::same(5_u8));
|
||||
ui.add(btn).clicked()
|
||||
}
|
||||
|
||||
// Render studio level VU Meter
|
||||
fn render_vu_meter(ui: &mut egui::Ui, level_pct: f32, active: bool, anim_phase: f32) {
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 16.0), egui::Sense::hover());
|
||||
let painter = ui.painter();
|
||||
|
||||
// Background track
|
||||
painter.rect_filled(rect, CornerRadius::same(3_u8), Color32::from_rgb(13, 17, 24));
|
||||
painter.rect_stroke(rect, CornerRadius::same(3_u8), Stroke::new(1.0_f32, Color32::from_rgb(28, 36, 52)), egui::StrokeKind::Inside);
|
||||
|
||||
if !active {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamic animated activity simulation based on gain level
|
||||
let wave = ((anim_phase * 6.0).sin() * 0.15 + (anim_phase * 14.0).cos() * 0.10).max(-0.2);
|
||||
let fill_ratio = ((level_pct / 100.0) * (0.65 + wave)).clamp(0.02, 1.0);
|
||||
let filled_width = rect.width() * fill_ratio;
|
||||
|
||||
// Segmented bar styling
|
||||
let num_segments = 24;
|
||||
let seg_w = (rect.width() - (num_segments as f32 - 1.0) * 2.0) / num_segments as f32;
|
||||
for i in 0..num_segments {
|
||||
let seg_x = rect.min.x + i as f32 * (seg_w + 2.0);
|
||||
if seg_x > rect.min.x + filled_width {
|
||||
break;
|
||||
}
|
||||
let seg_rect = egui::Rect::from_min_size(egui::pos2(seg_x, rect.min.y + 2.0), Vec2::new(seg_w, rect.height() - 4.0));
|
||||
let pos_frac = i as f32 / num_segments as f32;
|
||||
let seg_color = if pos_frac < 0.65 {
|
||||
Color32::from_rgb(34, 197, 94) // Green (-40dB to -12dB)
|
||||
} else if pos_frac < 0.85 {
|
||||
Color32::from_rgb(234, 179, 8) // Yellow (-12dB to -3dB)
|
||||
} else {
|
||||
Color32::from_rgb(239, 68, 68) // Red (Limit/Peak)
|
||||
};
|
||||
painter.rect_filled(seg_rect, CornerRadius::same(2_u8), seg_color);
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for VrecOverlayApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_async_events();
|
||||
ctx.request_repaint_after(Duration::from_millis(100));
|
||||
self.anim_time += 0.033;
|
||||
ctx.request_repaint_after(Duration::from_millis(50));
|
||||
|
||||
// Handle Escape key to close
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
|
|
@ -241,27 +302,39 @@ impl eframe::App for VrecOverlayApp {
|
|||
|
||||
// Tab Switcher Pills
|
||||
let settings_active = self.current_tab == OverlayTab::Settings;
|
||||
let settings_fill = if settings_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 29, 41) };
|
||||
let settings_stroke = if settings_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) };
|
||||
let settings_btn = egui::Button::new(egui::RichText::new("Settings").size(12.0).color(Color32::WHITE))
|
||||
let settings_fill = if settings_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(20, 26, 38) };
|
||||
let settings_stroke = if settings_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) };
|
||||
let settings_btn = egui::Button::new(egui::RichText::new("Settings").size(11.5).color(Color32::WHITE))
|
||||
.fill(settings_fill)
|
||||
.stroke(settings_stroke)
|
||||
.corner_radius(CornerRadius::same(6_u8));
|
||||
if ui.add_sized([78.0, 26.0], settings_btn).clicked() {
|
||||
if ui.add_sized([72.0, 26.0], settings_btn).clicked() {
|
||||
self.current_tab = OverlayTab::Settings;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 490.0)));
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 520.0)));
|
||||
}
|
||||
|
||||
let audio_active = self.current_tab == OverlayTab::AudioMixer;
|
||||
let audio_fill = if audio_active { Color32::from_rgb(124, 58, 237) } else { Color32::from_rgb(20, 26, 38) };
|
||||
let audio_stroke = if audio_active { Stroke::new(1.0_f32, Color32::from_rgb(168, 85, 247)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) };
|
||||
let audio_btn = egui::Button::new(egui::RichText::new("Audio Mixer").size(11.5).color(Color32::WHITE))
|
||||
.fill(audio_fill)
|
||||
.stroke(audio_stroke)
|
||||
.corner_radius(CornerRadius::same(6_u8));
|
||||
if ui.add_sized([86.0, 26.0], audio_btn).clicked() {
|
||||
self.current_tab = OverlayTab::AudioMixer;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 480.0)));
|
||||
}
|
||||
|
||||
let dash_active = self.current_tab == OverlayTab::Dashboard;
|
||||
let dash_fill = if dash_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 29, 41) };
|
||||
let dash_stroke = if dash_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) };
|
||||
let dash_btn = egui::Button::new(egui::RichText::new("Dashboard").size(12.0).color(Color32::WHITE))
|
||||
let dash_fill = if dash_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(20, 26, 38) };
|
||||
let dash_stroke = if dash_active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(40, 50, 72)) };
|
||||
let dash_btn = egui::Button::new(egui::RichText::new("Dashboard").size(11.5).color(Color32::WHITE))
|
||||
.fill(dash_fill)
|
||||
.stroke(dash_stroke)
|
||||
.corner_radius(CornerRadius::same(6_u8));
|
||||
if ui.add_sized([84.0, 26.0], dash_btn).clicked() {
|
||||
if ui.add_sized([80.0, 26.0], dash_btn).clicked() {
|
||||
self.current_tab = OverlayTab::Dashboard;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 280.0)));
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(840.0, 310.0)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -295,47 +368,47 @@ impl eframe::App for VrecOverlayApp {
|
|||
let replay_border = if self.status.is_replay_active {
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(16, 185, 129))
|
||||
} else {
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60))
|
||||
};
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(replay_border)
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(14_i8))
|
||||
.show(&mut cols[0], |ui| {
|
||||
ui.set_min_height(140.0);
|
||||
ui.set_min_height(160.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("INSTANT REPLAY").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
let buf_label = format!("Rolling Buffer: {}s", self.config.replay_duration_sec);
|
||||
ui.label(egui::RichText::new("INSTANT REPLAY").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
let buf_label = format!("Buffer: {} seconds in RAM", self.config.replay_duration_sec);
|
||||
ui.label(egui::RichText::new(buf_label).font(FontId::proportional(11.0)).color(Color32::from_rgb(56, 189, 248)));
|
||||
ui.add_space(12.0);
|
||||
let save_btn = egui::Button::new(egui::RichText::new("Save Replay").strong().color(Color32::WHITE))
|
||||
ui.add_space(14.0);
|
||||
let save_btn = egui::Button::new(egui::RichText::new("Save Instant Replay").strong().color(Color32::WHITE))
|
||||
.fill(Color32::from_rgb(16, 185, 129))
|
||||
.corner_radius(CornerRadius::same(7_u8));
|
||||
if ui.add_sized([160.0, 34.0], save_btn).clicked() {
|
||||
if ui.add_sized([180.0, 36.0], save_btn).clicked() {
|
||||
let _ = ipc::send_command(Command::SaveReplay);
|
||||
self.set_msg("Replay save command sent");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.add_space(10.0);
|
||||
render_keycap(ui, &self.config.save_hotkey);
|
||||
});
|
||||
});
|
||||
|
||||
// Card 2: Screen Record
|
||||
// Card 2: Screen Recording
|
||||
let rec_border = if self.status.is_recording {
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(239, 68, 68))
|
||||
} else {
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58))
|
||||
Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60))
|
||||
};
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(rec_border)
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(14_i8))
|
||||
.show(&mut cols[1], |ui| {
|
||||
ui.set_min_height(140.0);
|
||||
ui.set_min_height(160.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("SCREEN RECORDING").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
ui.label(egui::RichText::new("SCREEN RECORDING").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
let timer_text = if self.status.is_recording {
|
||||
let d = self.status.recording_duration_sec;
|
||||
format!("Active: {:02}:{:02}:{:02}", d / 3600, (d % 3600) / 60, d % 60)
|
||||
|
|
@ -344,68 +417,72 @@ impl eframe::App for VrecOverlayApp {
|
|||
};
|
||||
let timer_color = if self.status.is_recording { Color32::from_rgb(248, 113, 113) } else { Color32::from_rgb(100, 116, 139) };
|
||||
ui.label(egui::RichText::new(timer_text).font(FontId::proportional(11.0)).color(timer_color));
|
||||
ui.add_space(12.0);
|
||||
ui.add_space(14.0);
|
||||
let (rec_label, rec_color) = if self.status.is_recording {
|
||||
("Stop Recording", Color32::from_rgb(220, 38, 38))
|
||||
} else {
|
||||
("Start Record", Color32::from_rgb(37, 99, 235))
|
||||
("Start Recording", Color32::from_rgb(37, 99, 235))
|
||||
};
|
||||
let rec_btn = egui::Button::new(egui::RichText::new(rec_label).strong().color(Color32::WHITE))
|
||||
.fill(rec_color)
|
||||
.corner_radius(CornerRadius::same(7_u8));
|
||||
if ui.add_sized([160.0, 34.0], rec_btn).clicked() {
|
||||
if ui.add_sized([180.0, 36.0], rec_btn).clicked() {
|
||||
let _ = ipc::send_command(Command::ToggleRecording);
|
||||
self.set_msg("Recording toggled");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.add_space(10.0);
|
||||
render_keycap(ui, &self.config.record_hotkey);
|
||||
});
|
||||
});
|
||||
|
||||
// Card 3: Audio Route
|
||||
// Card 3: Audio Deck
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(32, 42, 60)))
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(14_i8))
|
||||
.show(&mut cols[2], |ui| {
|
||||
ui.set_min_height(140.0);
|
||||
ui.set_min_height(160.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(egui::RichText::new("AUDIO ROUTE").font(FontId::proportional(12.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
ui.label(egui::RichText::new("AUDIO & CURSOR").font(FontId::monospace(11.0)).strong().color(Color32::from_rgb(148, 163, 184)));
|
||||
let mode_name = match self.status.audio_mode.as_str() {
|
||||
"mic" => "Microphone Only",
|
||||
"both" => "System + Mic (Mixed)",
|
||||
"muted" => "Muted",
|
||||
_ => "System Audio Only",
|
||||
"mic" => format!("Mic Only ({}%)", self.mic_volume_pct),
|
||||
"both" => format!("System + Mic ({}%)", self.mic_volume_pct),
|
||||
"muted" => "Muted".to_string(),
|
||||
_ => format!("System Audio ({}%)", self.system_volume_pct),
|
||||
};
|
||||
ui.label(egui::RichText::new(mode_name).font(FontId::proportional(11.0)).color(Color32::from_rgb(168, 85, 247)));
|
||||
ui.add_space(12.0);
|
||||
ui.add_space(14.0);
|
||||
let audio_btn = egui::Button::new(egui::RichText::new("Cycle Audio Mode").strong().color(Color32::from_rgb(226, 232, 240)))
|
||||
.fill(Color32::from_rgb(30, 41, 59))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(51, 65, 85)))
|
||||
.fill(Color32::from_rgb(28, 36, 52))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(46, 60, 86)))
|
||||
.corner_radius(CornerRadius::same(7_u8));
|
||||
if ui.add_sized([160.0, 34.0], audio_btn).clicked() {
|
||||
if ui.add_sized([180.0, 36.0], audio_btn).clicked() {
|
||||
let _ = ipc::send_command(Command::CycleAudioMode);
|
||||
self.set_msg("Audio mode cycled");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
render_keycap(ui, "Cycle");
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
render_keycap(ui, "Cycle");
|
||||
ui.add_space(2.0);
|
||||
render_keycap(ui, &self.config.cursor_hotkey);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Bottom info ribbon
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(14, 17, 24))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(28, 34, 48)))
|
||||
.fill(Color32::from_rgb(13, 16, 24))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(26, 32, 46)))
|
||||
.corner_radius(CornerRadius::same(6_u8))
|
||||
.inner_margin(Margin::symmetric(12_i8, 6_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("Hardware Engine: VAAPI / NV12 Zero-Copy • {} FPS • {} Mbps", self.config.fps, self.bitrate_mbps))
|
||||
egui::RichText::new(format!("Hardware Engine: VAAPI NV12 Direct • {} FPS • {} Mbps", self.config.fps, self.bitrate_mbps))
|
||||
.font(FontId::monospace(10.0))
|
||||
.color(Color32::from_rgb(100, 116, 139)),
|
||||
);
|
||||
|
|
@ -420,23 +497,192 @@ impl eframe::App for VrecOverlayApp {
|
|||
});
|
||||
}
|
||||
|
||||
// VIEW: SETTINGS
|
||||
if self.current_tab == OverlayTab::Settings {
|
||||
let pill = |ui: &mut egui::Ui, text: &str, active: bool| -> bool {
|
||||
let fill = if active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 30, 42) };
|
||||
let stroke = if active { Stroke::new(1.0_f32, Color32::from_rgb(96, 165, 250)) } else { Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75)) };
|
||||
let btn = egui::Button::new(egui::RichText::new(text).size(11.0).color(Color32::from_rgb(240, 246, 252)))
|
||||
.fill(fill)
|
||||
.stroke(stroke)
|
||||
.corner_radius(CornerRadius::same(5_u8));
|
||||
ui.add(btn).clicked()
|
||||
};
|
||||
// TAB 2: AUDIO MIXER & LEVELS (Dedicated Studio Audio Tab)
|
||||
if self.current_tab == OverlayTab::AudioMixer {
|
||||
egui::ScrollArea::vertical().max_height(380.0).show(ui, |ui| {
|
||||
// Master Channel Strips
|
||||
ui.columns(2, |ch_cols| {
|
||||
// Strip 1: Microphone Input
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(14_i8))
|
||||
.show(&mut ch_cols[0], |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("MICROPHONE INPUT")
|
||||
.font(FontId::proportional(13.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(56, 189, 248)),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{}%", self.mic_volume_pct))
|
||||
.font(FontId::monospace(13.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(56, 189, 248)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
egui::ScrollArea::vertical().max_height(350.0).show(ui, |ui| {
|
||||
// Section 1: Destination Directory (Addressed explicitly per user request)
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("PulseAudio/PipeWire Native Source (@DEFAULT_SOURCE@)")
|
||||
.font(FontId::monospace(9.0))
|
||||
.color(Color32::from_rgb(100, 116, 139)),
|
||||
);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.add(egui::Slider::new(&mut self.mic_volume_pct, 0..=150).text("Gain"));
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139)));
|
||||
if pill(ui, "30% Low", self.mic_volume_pct == 30) { self.mic_volume_pct = 30; }
|
||||
if pill(ui, "60% Optimal", self.mic_volume_pct == 60) { self.mic_volume_pct = 60; }
|
||||
if pill(ui, "80% Loud", self.mic_volume_pct == 80) { self.mic_volume_pct = 80; }
|
||||
if pill(ui, "100% Max", self.mic_volume_pct == 100) { self.mic_volume_pct = 100; }
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.label(egui::RichText::new("Live Input Peak Meter").font(FontId::proportional(10.5)).color(Color32::from_rgb(148, 163, 184)));
|
||||
let is_mic_active = self.status.audio_mode == "mic" || self.status.audio_mode == "both";
|
||||
render_vu_meter(ui, self.mic_volume_pct as f32, is_mic_active, self.anim_time);
|
||||
|
||||
ui.add_space(8.0);
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(16, 185, 129, 20))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(16, 185, 129)))
|
||||
.corner_radius(CornerRadius::same(5_u8))
|
||||
.inner_margin(Margin::symmetric(8_i8, 4_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("Anti-Clipping Soft-Knee Limiter Active")
|
||||
.font(FontId::monospace(9.5))
|
||||
.color(Color32::from_rgb(52, 211, 153)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Strip 2: System Audio Output
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(14_i8))
|
||||
.show(&mut ch_cols[1], |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("SYSTEM AUDIO MONITOR")
|
||||
.font(FontId::proportional(13.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(168, 85, 247)),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{}%", self.system_volume_pct))
|
||||
.font(FontId::monospace(13.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(168, 85, 247)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("PipeWire Audio Monitor Sink (@DEFAULT_MONITOR@)")
|
||||
.font(FontId::monospace(9.0))
|
||||
.color(Color32::from_rgb(100, 116, 139)),
|
||||
);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.add(egui::Slider::new(&mut self.system_volume_pct, 0..=150).text("Level"));
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139)));
|
||||
if pill(ui, "50% Half", self.system_volume_pct == 50) { self.system_volume_pct = 50; }
|
||||
if pill(ui, "75% Soft", self.system_volume_pct == 75) { self.system_volume_pct = 75; }
|
||||
if pill(ui, "100% Full", self.system_volume_pct == 100) { self.system_volume_pct = 100; }
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.label(egui::RichText::new("Live System Output Level").font(FontId::proportional(10.5)).color(Color32::from_rgb(148, 163, 184)));
|
||||
let is_sys_active = self.status.audio_mode != "muted" && self.status.audio_mode != "mic";
|
||||
render_vu_meter(ui, self.system_volume_pct as f32, is_sys_active, self.anim_time * 0.85);
|
||||
|
||||
ui.add_space(8.0);
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgba_unmultiplied(124, 58, 237, 20))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(139, 92, 246)))
|
||||
.corner_radius(CornerRadius::same(5_u8))
|
||||
.inner_margin(Margin::symmetric(8_i8, 4_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("Direct PulseAudio Hardware Interceptor")
|
||||
.font(FontId::monospace(9.5))
|
||||
.color(Color32::from_rgb(196, 181, 253)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Section 2: Audio Routing Selection
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(10_u8))
|
||||
.inner_margin(Margin::same(12_i8))
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("STREAM AUDIO ROUTING")
|
||||
.font(FontId::proportional(12.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(203, 213, 225)),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
if pill(ui, "System Audio Only", self.audio_mode_idx == 0) { self.audio_mode_idx = 0; }
|
||||
if pill(ui, "Microphone Only", self.audio_mode_idx == 1) { self.audio_mode_idx = 1; }
|
||||
if pill(ui, "Mixed (System + Mic)", self.audio_mode_idx == 2) { self.audio_mode_idx = 2; }
|
||||
if pill(ui, "Muted (Silent Video)", self.audio_mode_idx == 3) { self.audio_mode_idx = 3; }
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Quick Apply Button for Audio
|
||||
ui.horizontal(|ui| {
|
||||
let apply_audio_btn = egui::Button::new(egui::RichText::new("Apply Audio Mixer Settings").strong().color(Color32::WHITE))
|
||||
.fill(Color32::from_rgb(124, 58, 237))
|
||||
.corner_radius(CornerRadius::same(6_u8));
|
||||
if ui.add_sized([220.0, 32.0], apply_audio_btn).clicked() {
|
||||
self.config.mic_volume = self.mic_volume_pct as f32 / 100.0;
|
||||
self.config.system_volume = self.system_volume_pct as f32 / 100.0;
|
||||
self.config.audio_mode = match self.audio_mode_idx {
|
||||
1 => "mic",
|
||||
2 => "both",
|
||||
3 => "muted",
|
||||
_ => "system",
|
||||
}.to_string();
|
||||
let _ = self.config.save();
|
||||
let _ = ipc::send_command(Command::ReloadConfig);
|
||||
self.set_msg("Audio mixer levels saved and reloaded!");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TAB 3: SETTINGS
|
||||
if self.current_tab == OverlayTab::Settings {
|
||||
egui::ScrollArea::vertical().max_height(410.0).show(ui, |ui| {
|
||||
// Section 1: Save Destination Directory
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(8_u8))
|
||||
.inner_margin(Margin::same(12_i8))
|
||||
.show(ui, |ui| {
|
||||
|
|
@ -449,9 +695,8 @@ impl eframe::App for VrecOverlayApp {
|
|||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Folder Path:").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225)));
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(380.0));
|
||||
ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(420.0));
|
||||
|
||||
// Browse button: opens native folder dialog asynchronously
|
||||
let browse_btn = egui::Button::new(egui::RichText::new("Browse...").color(Color32::WHITE))
|
||||
.fill(Color32::from_rgb(37, 99, 235))
|
||||
.corner_radius(CornerRadius::same(5_u8));
|
||||
|
|
@ -496,24 +741,22 @@ impl eframe::App for VrecOverlayApp {
|
|||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Presets:").font(FontId::proportional(10.0)).color(Color32::from_rgb(100, 116, 139)));
|
||||
let v_dir = format!("{}/Videos/vrec", std::env::var("HOME").unwrap_or_default());
|
||||
let c_dir = format!("{}/Videos/Captures", std::env::var("HOME").unwrap_or_default());
|
||||
let d_dir = format!("{}/Downloads/vrec", std::env::var("HOME").unwrap_or_default());
|
||||
if pill(ui, "~/Videos/vrec", self.output_dir == v_dir) {
|
||||
self.output_dir = v_dir;
|
||||
}
|
||||
if pill(ui, "~/Downloads/vrec", self.output_dir == d_dir) {
|
||||
self.output_dir = d_dir;
|
||||
}
|
||||
if pill(ui, "~/Videos/vrec", self.output_dir == v_dir) { self.output_dir = v_dir; }
|
||||
if pill(ui, "~/Videos/Captures", self.output_dir == c_dir) { self.output_dir = c_dir; }
|
||||
if pill(ui, "~/Downloads/vrec", self.output_dir == d_dir) { self.output_dir = d_dir; }
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Section 2: Split 2 columns for Video & Audio Tuning
|
||||
// Section 2: Split Columns for Video and Replay
|
||||
ui.columns(2, |settings_cols| {
|
||||
// Column 1: Video & Encoding
|
||||
// Column 1: Video Quality & Cursor
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(8_u8))
|
||||
.inner_margin(Margin::same(12_i8))
|
||||
.show(&mut settings_cols[0], |ui| {
|
||||
|
|
@ -560,15 +803,15 @@ impl eframe::App for VrecOverlayApp {
|
|||
ui.label(egui::RichText::new("Codec: Hardware VAAPI H.264 (NV12 Direct)").font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139)));
|
||||
});
|
||||
|
||||
// Column 2: Replay & Audio Routing
|
||||
// Column 2: Replay Buffer & Hotkeys
|
||||
egui::Frame::NONE
|
||||
.fill(Color32::from_rgb(16, 20, 29))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 42, 58)))
|
||||
.fill(Color32::from_rgb(15, 19, 28))
|
||||
.stroke(Stroke::new(1.0_f32, Color32::from_rgb(34, 44, 64)))
|
||||
.corner_radius(CornerRadius::same(8_u8))
|
||||
.inner_margin(Margin::same(12_i8))
|
||||
.show(&mut settings_cols[1], |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new("REPLAY & AUDIO")
|
||||
egui::RichText::new("REPLAY BUFFER & HOTKEYS")
|
||||
.font(FontId::proportional(12.0))
|
||||
.strong()
|
||||
.color(Color32::from_rgb(168, 85, 247)),
|
||||
|
|
@ -576,7 +819,7 @@ impl eframe::App for VrecOverlayApp {
|
|||
ui.add_space(8.0);
|
||||
|
||||
// Replay Duration
|
||||
ui.label(egui::RichText::new("Replay Buffer Duration").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225)));
|
||||
ui.label(egui::RichText::new("Buffer Duration").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225)));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut self.replay_sec).range(5..=600).suffix(" s"));
|
||||
if pill(ui, "30s", self.replay_sec == 30) { self.replay_sec = 30; }
|
||||
|
|
@ -585,26 +828,23 @@ impl eframe::App for VrecOverlayApp {
|
|||
if pill(ui, "300s", self.replay_sec == 300) { self.replay_sec = 300; }
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Audio Mode
|
||||
ui.label(egui::RichText::new("Audio Source Routing").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225)));
|
||||
egui::ComboBox::from_id_salt("audio_routing_cb")
|
||||
.selected_text(match self.audio_mode_idx {
|
||||
1 => "Microphone Only",
|
||||
2 => "Both (System + Mic)",
|
||||
3 => "Muted",
|
||||
_ => "System Audio Only",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.audio_mode_idx, 0, "System Audio Only");
|
||||
ui.selectable_value(&mut self.audio_mode_idx, 1, "Microphone Only");
|
||||
ui.selectable_value(&mut self.audio_mode_idx, 2, "Both (System + Mic)");
|
||||
ui.selectable_value(&mut self.audio_mode_idx, 3, "Muted");
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("PulseAudio/PipeWire Native Capture (48kHz)").font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139)));
|
||||
ui.add_space(12.0);
|
||||
ui.label(egui::RichText::new("Configured Global Hotkeys").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225)));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Menu:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184)));
|
||||
render_keycap(ui, &self.config.menu_hotkey);
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Save:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184)));
|
||||
render_keycap(ui, &self.config.save_hotkey);
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Record:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184)));
|
||||
render_keycap(ui, &self.config.record_hotkey);
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Cursor:").font(FontId::monospace(10.0)).color(Color32::from_rgb(148, 163, 184)));
|
||||
render_keycap(ui, &self.config.cursor_hotkey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -615,7 +855,7 @@ impl eframe::App for VrecOverlayApp {
|
|||
let apply_btn = egui::Button::new(egui::RichText::new("Save & Apply All Settings").strong().color(Color32::WHITE))
|
||||
.fill(Color32::from_rgb(37, 99, 235))
|
||||
.corner_radius(CornerRadius::same(6_u8));
|
||||
if ui.add_sized([220.0, 34.0], apply_btn).clicked() {
|
||||
if ui.add_sized([230.0, 34.0], apply_btn).clicked() {
|
||||
self.config.output_directory = self.output_dir.clone();
|
||||
let _ = std::fs::create_dir_all(&self.output_dir);
|
||||
self.config.replay_duration_sec = self.replay_sec;
|
||||
|
|
@ -623,6 +863,8 @@ impl eframe::App for VrecOverlayApp {
|
|||
self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000;
|
||||
self.config.fps = self.target_fps;
|
||||
self.config.show_cursor = self.show_cursor;
|
||||
self.config.mic_volume = self.mic_volume_pct as f32 / 100.0;
|
||||
self.config.system_volume = self.system_volume_pct as f32 / 100.0;
|
||||
self.config.audio_mode = match self.audio_mode_idx {
|
||||
1 => "mic",
|
||||
2 => "both",
|
||||
|
|
@ -653,9 +895,9 @@ pub fn run_egui_overlay() {
|
|||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("vrec")
|
||||
.with_app_id("vrec-overlay")
|
||||
.with_inner_size([820.0, 280.0])
|
||||
.with_min_inner_size([720.0, 240.0])
|
||||
.with_max_inner_size([920.0, 560.0])
|
||||
.with_inner_size([840.0, 310.0])
|
||||
.with_min_inner_size([720.0, 260.0])
|
||||
.with_max_inner_size([960.0, 600.0])
|
||||
.with_resizable(true)
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue