use super::render_util::{
apply_rounded_shape, blit_glyph_at, compute_spectrum, map_spectrum_to_column, rasterise_glyphs,
rms, PixelBuffer, RingBuffer, FFT_SIZE, FREQ_MAX, FREQ_NOISE_FLOOR, PEAK_DECAY, PEAK_FLOOR,
};
use crate::error::TalkError;
use crate::telemetry::TranscriptionEvent;
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use x11rb::connection::Connection;
use x11rb::protocol::shape;
use x11rb::protocol::xproto::*;
use x11rb::wrapper::ConnectionExt as _;
use x11rb::COPY_DEPTH_FROM_PARENT;
const TRANSCRIBING_PNG: &[u8] = include_bytes!("../../assets/transcribing.png");
pub(crate) const BADGE_W: u16 = 273;
const BADGE_H: u16 = 52;
const CORNER_RADIUS: usize = 13;
const DOT_CX: usize = 26;
const DOT_CY: usize = 26;
const DOT_RADIUS_MIN: f32 = 6.0;
const DOT_RADIUS_MAX: f32 = 16.0;
const PROHIBIT_ICON_RADIUS: f32 = 16.0;
const DOT_MIN_BRIGHTNESS: f32 = 0.5;
const DOT_GAP: f32 = 2.0;
const SPEC_LEFT: usize = 4;
const SPEC_TOP: usize = 4;
const SPEC_RIGHT: usize = 269;
const SPEC_BOTTOM: usize = 48;
const SPEC_W: usize = SPEC_RIGHT - SPEC_LEFT;
const SPEC_H: usize = SPEC_BOTTOM - SPEC_TOP;
const FPS: u32 = 60;
const COLUMN_PERIOD_FRAMES: u32 = 2;
const CENTERED_HEIGHT_FRACTION: f32 = 0.05;
const CENTERED_MIN_HEIGHT: u16 = 50;
const CENTERED_ASPECT_RATIO: f32 = 5.0;
const CENTERED_CORNER_RADIUS: usize = 16;
const CENTERED_BG_ARGB: [u8; 4] = [0x00, 0x00, 0x00, 0xCC];
const CENTERED_BG_OPAQUE: [u8; 4] = [0x00, 0x00, 0x00, 0xFF];
const DIM_FACTOR_PAUSED: f32 = 0.3;
const GRID_PERIOD_SECONDS: u64 = 1;
const COLUMNS_PER_GRID_MARK: u64 = (FPS as u64 / COLUMN_PERIOD_FRAMES as u64) * GRID_PERIOD_SECONDS;
const GRID_BLEND_ALPHA: f32 = 0.6;
const FREQ_INITIAL_MAX: f32 = 320.0;
const BG_COLOR: [u8; 4] = [0x00, 0x00, 0x00, 0xFF];
const BORDER_COLOR: [u8; 4] = [0x88, 0x88, 0x88, 0xFF];
const BORDER_WIDTH: f32 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndicatorKind {
Recording,
Transcribing,
DownloadingModel,
}
enum Command {
Show(IndicatorKind),
Hide,
Quit,
}
pub struct OverlayHandle {
tx: mpsc::Sender<Command>,
thread: Option<std::thread::JoinHandle<()>>,
had_live_audio: Arc<std::sync::atomic::AtomicBool>,
}
impl OverlayHandle {
#[allow(clippy::too_many_arguments)] pub fn new(
viz: Option<crate::config::VizMode>,
mono: bool,
audio_ring: Arc<Mutex<RingBuffer>>,
sample_rate: u32,
silence_tx: Option<std::sync::mpsc::Sender<bool>>,
pause_flag: Arc<std::sync::atomic::AtomicBool>,
auto_pause: bool,
telemetry_rx: Option<tokio::sync::broadcast::Receiver<TranscriptionEvent>>,
) -> Result<Self, TalkError> {
let geom = super::monitor::primary_monitor_geometry()?;
let mono_palette = if mono {
let (fg, bg) = super::render_util::monochrome_palette();
log::info!("monochrome overlay: fg={:?} bg={:?}", fg, bg);
Some((fg, bg))
} else {
None
};
let (tx, rx) = mpsc::channel();
let had_live_audio = Arc::new(std::sync::atomic::AtomicBool::new(false));
let had_live_audio_clone = Arc::clone(&had_live_audio);
let thread = std::thread::Builder::new()
.name("overlay".into())
.spawn(move || {
if let Err(e) = overlay_thread(
rx,
geom,
viz,
mono_palette,
audio_ring,
sample_rate,
silence_tx,
pause_flag,
auto_pause,
telemetry_rx,
had_live_audio_clone,
) {
log::error!("overlay thread error: {}", e);
}
})
.map_err(|e| TalkError::Audio(format!("failed to spawn overlay thread: {}", e)))?;
Ok(Self {
tx,
thread: Some(thread),
had_live_audio,
})
}
pub fn show(&self, kind: IndicatorKind) {
let _ = self.tx.send(Command::Show(kind));
}
pub fn hide(&self) {
let _ = self.tx.send(Command::Hide);
}
pub fn had_live_audio(&self) -> bool {
self.had_live_audio
.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Drop for OverlayHandle {
fn drop(&mut self) {
let _ = self.tx.send(Command::Quit);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
struct RgbaImage {
width: u32,
height: u32,
data: Vec<u8>,
}
fn is_stuck_at_rail(samples: &[f32], rail_floor: f32, flat_eps: f32) -> bool {
if samples.is_empty() {
return false;
}
let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
for &s in samples {
if s < lo {
lo = s;
}
if s > hi {
hi = s;
}
}
let flat = (hi - lo) <= flat_eps;
let near_rail = lo.abs().max(hi.abs()) >= rail_floor;
flat && near_rail
}
fn decode_png(bytes: &[u8]) -> Result<RgbaImage, TalkError> {
let decoder = png::Decoder::new(bytes);
let mut reader = decoder
.read_info()
.map_err(|e| TalkError::Config(format!("failed to read PNG header: {}", e)))?;
let mut buf = vec![0u8; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buf)
.map_err(|e| TalkError::Config(format!("failed to decode PNG frame: {}", e)))?;
let data = match info.color_type {
png::ColorType::Rgba => buf[..info.buffer_size()].to_vec(),
png::ColorType::Rgb => {
let rgb = &buf[..info.buffer_size()];
let mut rgba = Vec::with_capacity(info.width as usize * info.height as usize * 4);
for chunk in rgb.chunks_exact(3) {
rgba.extend_from_slice(chunk);
rgba.push(255);
}
rgba
}
other => {
return Err(TalkError::Config(format!(
"unsupported PNG color type: {:?}",
other
)));
}
};
Ok(RgbaImage {
width: info.width,
height: info.height,
data,
})
}
#[allow(clippy::too_many_arguments)]
fn render_spectrogram(
pb: &mut PixelBuffer,
history: &[Vec<f32>],
x0: usize,
y0: usize,
w: usize,
h: usize,
peak: f32,
mono: Option<([u8; 4], [u8; 4])>,
dim: f32,
) {
if history.is_empty() || peak < PEAK_FLOOR {
return;
}
let dim = dim.clamp(0.0, 1.0);
let n = history.len();
let start = n.saturating_sub(w);
let num_cols = n - start;
for (col_idx, column) in history[start..].iter().enumerate() {
let x = x0 + w - num_cols + col_idx;
for (row_idx, &magnitude) in column.iter().enumerate() {
if row_idx >= h {
break;
}
let y = y0 + h - 1 - row_idx;
let norm = (magnitude / peak).clamp(0.0, 1.0);
let brightness = if norm > 0.0 {
(1.0 + norm * 9.0).log10() } else {
0.0
};
let mut color = if mono.is_some() {
let alpha = (brightness * 255.0 * dim) as u8;
[alpha, alpha, alpha, alpha]
} else {
let c = super::render_util::heat_map_color(norm, brightness);
[
(c[0] as f32 * dim) as u8,
(c[1] as f32 * dim) as u8,
(c[2] as f32 * dim) as u8,
c[3],
]
};
color[3] = 0xFF;
pb.set_pixel(x, y, color);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_amplitude_badge(
pb: &mut PixelBuffer,
history: &[f32],
max_rms: f32,
x0: usize,
y0: usize,
w: usize,
h: usize,
mono: Option<([u8; 4], [u8; 4])>,
dim: f32,
) {
if history.is_empty() || max_rms < PEAK_FLOOR {
return;
}
let dim = dim.clamp(0.0, 1.0);
let n = history.len();
let center_y = y0 + h / 2;
let max_half = (h / 2).saturating_sub(1);
for col in 0..w {
let start = col * n / w;
let end = ((col + 1) * n / w).max(start + 1).min(n);
let avg_rms = if end > start {
history[start..end].iter().sum::<f32>() / (end - start) as f32
} else if start < n {
history[start]
} else {
0.0
};
let norm = (avg_rms / max_rms).clamp(0.0, 1.0);
let half_height = (norm * max_half as f32) as usize;
let base = if let Some((fg, bg)) = mono {
super::render_util::lerp_color(bg, fg, norm)
} else {
super::render_util::level_color(norm)
};
let color = [
(base[0] as f32 * dim) as u8,
(base[1] as f32 * dim) as u8,
(base[2] as f32 * dim) as u8,
base[3],
];
let top = center_y.saturating_sub(half_height);
let bottom = center_y + half_height;
for y in top..=bottom.min(y0 + h - 1) {
pb.set_pixel(x0 + col, y, color);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_spectrum_badge(
pb: &mut PixelBuffer,
magnitudes: &[f32],
peak: f32,
x0: usize,
y0: usize,
w: usize,
h: usize,
mono: Option<([u8; 4], [u8; 4])>,
dim: f32,
) {
if magnitudes.is_empty() || peak < PEAK_FLOOR {
return;
}
let dim = dim.clamp(0.0, 1.0);
let useful = &magnitudes[..magnitudes.len() / 4];
let num_bars = w; if num_bars == 0 || useful.is_empty() {
return;
}
for bar in 0..num_bars {
let start = bar * useful.len() / num_bars;
let end = ((bar + 1) * useful.len() / num_bars).max(start + 1);
let avg = if end > start {
useful[start..end].iter().sum::<f32>() / (end - start) as f32
} else {
0.0
};
let norm = (avg / peak).clamp(0.0, 1.0);
let log_norm = (1.0 + norm * 9.0).log10();
let bar_height = (log_norm * (h.saturating_sub(4)) as f32) as usize;
let bar_x = x0 + bar;
let bar_y = y0 + h - 2 - bar_height;
let base = if let Some((fg, bg)) = mono {
super::render_util::lerp_color(bg, fg, log_norm)
} else {
super::render_util::level_color(log_norm)
};
let color = [
(base[0] as f32 * dim) as u8,
(base[1] as f32 * dim) as u8,
(base[2] as f32 * dim) as u8,
base[3],
];
for dy in 0..bar_height {
pb.set_pixel(bar_x, bar_y + dy, color);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_time_grid(
pb: &mut PixelBuffer,
x0: usize,
y0: usize,
w: usize,
h: usize,
first_visible_abs_idx: u64,
num_visible_cols: usize,
columns_per_mark: u64,
) {
if num_visible_cols == 0 || columns_per_mark == 0 || w == 0 || h == 0 {
return;
}
let num = num_visible_cols.min(w);
let inv_alpha = 1.0 - GRID_BLEND_ALPHA;
const YELLOW_B: f32 = 0.0;
const YELLOW_G: f32 = 255.0;
const YELLOW_R: f32 = 255.0;
for col_idx in 0..num {
let abs_idx = first_visible_abs_idx + col_idx as u64;
if !abs_idx.is_multiple_of(columns_per_mark) {
continue;
}
let x = x0 + w - num + col_idx;
if x >= pb.width {
continue;
}
let mut row = 0usize;
while row < h {
let y = y0 + row;
if y < pb.height {
let off = (y * pb.width + x) * 4;
let b = pb.data[off] as f32;
let g = pb.data[off + 1] as f32;
let r = pb.data[off + 2] as f32;
pb.data[off] = (b * inv_alpha + YELLOW_B * GRID_BLEND_ALPHA) as u8;
pb.data[off + 1] = (g * inv_alpha + YELLOW_G * GRID_BLEND_ALPHA) as u8;
pb.data[off + 2] = (r * inv_alpha + YELLOW_R * GRID_BLEND_ALPHA) as u8;
}
row += 2;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Idle,
Validating,
Connecting,
Uploading,
WaitingResponse,
Receiving,
WaitingRetry,
Done,
Error,
}
const PHASE_GREEN: [u8; 4] = [110, 255, 60, 255];
const PHASE_AMBER: [u8; 4] = [50, 180, 255, 255];
const BACKOFF_TRAIL_OPACITY: f32 = 0.35;
const VALIDATING_OPACITY: f32 = 0.5;
impl Phase {
fn color(self) -> Option<([u8; 4], f32)> {
match self {
Self::Idle => None,
Self::Validating => Some((PHASE_GREEN, VALIDATING_OPACITY)),
Self::Connecting => Some(([200, 130, 80, 255], 1.0)),
Self::Uploading => Some(([255, 170, 60, 255], 1.0)),
Self::WaitingResponse => Some((PHASE_AMBER, 1.0)),
Self::WaitingRetry => Some((PHASE_AMBER, BACKOFF_TRAIL_OPACITY)),
Self::Receiving => Some(([180, 200, 60, 255], 1.0)),
Self::Done => Some((PHASE_GREEN, 1.0)),
Self::Error => Some(([40, 40, 200, 255], 1.0)),
}
}
fn opacity(self) -> f32 {
self.color().map(|(_, op)| op).unwrap_or(1.0)
}
fn advance(self, event: &TranscriptionEvent) -> Self {
match event {
TranscriptionEvent::PreflightStarted { .. } => Self::Validating,
TranscriptionEvent::PreflightCompleted { success: true, .. } => {
Self::Idle
}
TranscriptionEvent::PreflightCompleted { success: false, .. } => Self::Error,
TranscriptionEvent::RequestStarted { .. } => Self::Connecting,
TranscriptionEvent::ConnectionEstablished { .. } => Self::Uploading,
TranscriptionEvent::UploadComplete { .. } => Self::WaitingResponse,
TranscriptionEvent::ResponseHeaders { .. } => Self::Receiving,
TranscriptionEvent::RequestCompleted { success: true, .. } => Self::Done,
TranscriptionEvent::RequestCompleted { success: false, .. } => Self::Error,
TranscriptionEvent::RetryScheduled { kind, .. } => match (self, kind) {
(Self::Validating, _) => Self::Validating,
(_, crate::telemetry::RetryKind::Data) => Self::WaitingRetry,
(_, crate::telemetry::RetryKind::Connection) => Self::Connecting,
},
TranscriptionEvent::PasteStarted { .. } => Self::Done, TranscriptionEvent::Done { .. } | TranscriptionEvent::PasteCompleted { .. } => {
Self::Idle
}
TranscriptionEvent::Failed { .. } => Self::Error,
_ => self,
}
}
}
const PHASE_LINE_HEIGHT: usize = 2;
fn render_phase_line(
pb: &mut PixelBuffer,
x0: usize,
y0: usize,
w: usize,
phase_history: &[Option<([u8; 4], f32)>],
) {
let n = phase_history.len();
if n == 0 || w == 0 {
return;
}
let num = n.min(w);
for col_idx in 0..num {
let (color, opacity) = match phase_history[n - num + col_idx] {
Some(t) => t,
None => continue,
};
let x = x0 + w - num + col_idx;
if x >= pb.width {
continue;
}
for dy in 0..PHASE_LINE_HEIGHT {
let y = y0 + dy;
if y >= pb.height {
continue;
}
if opacity >= 1.0 {
pb.set_pixel(x, y, color);
} else {
pb.blend_pixel(x, y, color, opacity);
}
}
}
}
fn backoff_remaining_fraction(
now: std::time::Instant,
started: std::time::Instant,
delay: std::time::Duration,
) -> f32 {
if delay.is_zero() {
return 0.0;
}
let elapsed = now.saturating_duration_since(started).as_secs_f32();
(1.0 - elapsed / delay.as_secs_f32()).clamp(0.0, 1.0)
}
fn render_backoff_bar(pb: &mut PixelBuffer, x0: usize, y0: usize, w: usize, fraction: f32) {
let filled = (w as f32 * fraction.clamp(0.0, 1.0)).round() as usize;
if filled == 0 {
return;
}
pb.fill_rect(x0, y0, filled.min(w), PHASE_LINE_HEIGHT, PHASE_AMBER);
}
fn backoff_badge_label(attempt: u32, max: u32) -> String {
format!("BUSY · RETRY {}/{}", attempt, max)
}
fn render_backoff_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
retry: (u32, u32),
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let label = backoff_badge_label(retry.0, retry.1);
render_badge_text(pb, font, &label, PHASE_AMBER, x0, y0, w, h);
}
const TRACK_HEIGHT_PX: usize = 16;
#[allow(clippy::too_many_arguments)]
fn render_throughput_tracks(
pb: &mut PixelBuffer,
x0: usize,
y0: usize,
w: usize,
h: usize,
upload_history: &[u64],
download_history: &[u64],
paste_history: &[u64],
_phase_history: &[Option<([u8; 4], f32)>],
upload_peak: u64,
download_peak: u64,
paste_peak: u64,
) {
if w == 0 || h == 0 {
return;
}
let num = upload_history
.len()
.min(download_history.len())
.min(paste_history.len())
.min(w);
if num == 0 {
return;
}
let track_h = TRACK_HEIGHT_PX.min(h / 3) as f32;
let bar_zone_top = y0 + PHASE_LINE_HEIGHT;
let bar_zone_bottom = y0 + h - 1;
let bar_zone_center = y0 + h / 2;
let upload_color: [u8; 4] = Phase::Uploading
.color()
.map(|(c, _)| c)
.unwrap_or([255, 170, 60, 255]);
let download_color: [u8; 4] = Phase::Receiving
.color()
.map(|(c, _)| c)
.unwrap_or([180, 200, 60, 255]);
let paste_color: [u8; 4] = Phase::Done
.color()
.map(|(c, _)| c)
.unwrap_or([110, 255, 60, 255]);
for col_idx in 0..num {
let x = x0 + w - num + col_idx;
if x >= pb.width {
continue;
}
let ui = upload_history.len() - num + col_idx;
let di = download_history.len() - num + col_idx;
let pi = paste_history.len() - num + col_idx;
if upload_history[ui] > 0 && upload_peak > 0 {
let norm = (upload_history[ui] as f32 / upload_peak as f32).clamp(0.0, 1.0);
let bar_h = ((norm * track_h) as usize).max(1);
for dy in 0..bar_h {
let y = bar_zone_top + dy;
if y >= pb.height || y > bar_zone_bottom {
break;
}
pb.set_pixel(x, y, upload_color);
}
}
if download_history[di] > 0 && download_peak > 0 {
let norm = (download_history[di] as f32 / download_peak as f32).clamp(0.0, 1.0);
let bar_h = ((norm * track_h) as usize).max(1);
for dy in 0..bar_h {
let y = bar_zone_bottom.saturating_sub(dy);
if y < y0 || y < bar_zone_top {
break;
}
pb.set_pixel(x, y, download_color);
}
}
if paste_history[pi] > 0 && paste_peak > 0 {
let norm = (paste_history[pi] as f32 / paste_peak as f32).clamp(0.0, 1.0);
let bar_h = ((norm * track_h) as usize).max(1);
let half = bar_h / 2;
for dy in 0..bar_h {
let y = bar_zone_center.saturating_sub(half) + dy;
if y >= pb.height || y > bar_zone_bottom || y < bar_zone_top {
continue;
}
pb.set_pixel(x, y, paste_color);
}
}
}
}
fn clear_dot_gap(pb: &mut PixelBuffer, cx: usize, cy: usize, outer_radius: f32) {
let r_sq = outer_radius * outer_radius;
let r_int = outer_radius.ceil() as usize;
for dy in 0..=r_int {
for dx in 0..=r_int {
let dist_sq = (dx * dx + dy * dy) as f32;
if dist_sq > r_sq {
continue;
}
let edge_dist = outer_radius - dist_sq.sqrt();
let edge_alpha = edge_dist.clamp(0.0, 1.0);
let coords: [(usize, usize); 4] = [
(cx + dx, cy + dy),
(cx.wrapping_sub(dx), cy + dy),
(cx + dx, cy.wrapping_sub(dy)),
(cx.wrapping_sub(dx), cy.wrapping_sub(dy)),
];
for (px, py) in coords {
if px < pb.width && py < pb.height {
if edge_alpha >= 1.0 {
pb.set_pixel(px, py, BG_COLOR);
} else {
let off = (py * pb.width + px) * 4;
let keep = 1.0 - edge_alpha;
pb.data[off] = (pb.data[off] as f32 * keep) as u8;
pb.data[off + 1] = (pb.data[off + 1] as f32 * keep) as u8;
pb.data[off + 2] = (pb.data[off + 2] as f32 * keep) as u8;
pb.data[off + 3] = (pb.data[off + 3] as f32 * keep) as u8;
}
}
}
}
}
}
fn draw_pulsing_dot(pb: &mut PixelBuffer, cx: usize, cy: usize, radius: f32, brightness: f32) {
let r_sq = radius * radius;
let r_int = radius.ceil() as usize;
let brightness = brightness.clamp(DOT_MIN_BRIGHTNESS, 1.0);
for dy in 0..=r_int {
for dx in 0..=r_int {
let dist_sq = (dx * dx + dy * dy) as f32;
if dist_sq > r_sq {
continue;
}
let edge_dist = radius - dist_sq.sqrt();
let edge_alpha = edge_dist.clamp(0.0, 1.0);
let val = (255.0 * brightness * edge_alpha) as u8;
let color = [0x00, 0x00, val, val];
let coords: [(usize, usize); 4] = [
(cx + dx, cy + dy),
(cx.wrapping_sub(dx), cy + dy),
(cx + dx, cy.wrapping_sub(dy)),
(cx.wrapping_sub(dx), cy.wrapping_sub(dy)),
];
for (px, py) in coords {
if px < pb.width && py < pb.height {
pb.set_pixel(px, py, color);
}
}
}
}
}
fn rounded_rect_sdf(px: f32, py: f32, w: f32, h: f32, r: f32) -> f32 {
let cx = px - w / 2.0;
let cy = py - h / 2.0;
let hw = w / 2.0 - r;
let hh = h / 2.0 - r;
let dx = cx.abs() - hw;
let dy = cy.abs() - hh;
let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
let inside = dx.max(dy).min(0.0);
outside + inside - r
}
fn draw_rounded_border(pb: &mut PixelBuffer, color: [u8; 4], radius: f32, border_width: f32) {
let w = pb.width as f32;
let h = pb.height as f32;
for y in 0..pb.height {
for x in 0..pb.width {
let d = rounded_rect_sdf(x as f32 + 0.5, y as f32 + 0.5, w, h, radius);
let outer_alpha = (-d).clamp(0.0, 1.0);
let inner_alpha = (d + border_width).clamp(0.0, 1.0);
let alpha = outer_alpha * inner_alpha;
if alpha > 0.0 {
let a = (color[3] as f32 * alpha) as u8;
let b = (color[0] as f32 * alpha) as u8;
let g = (color[1] as f32 * alpha) as u8;
let r = (color[2] as f32 * alpha) as u8;
let pixel = [b, g, r, a];
pb.set_pixel(x, y, pixel);
}
}
}
}
fn draw_prohibit_icon_with_stroke(
pb: &mut PixelBuffer,
cx: usize,
cy: usize,
radius: f32,
stroke: f32,
) {
let color: [u8; 4] = [0x00, 0x00, 0xFF, 0xFF]; let r_outer = radius;
let r_inner = radius - stroke;
let r_outer_sq = r_outer * r_outer;
let r_inner_sq = r_inner * r_inner;
let r_int = r_outer.ceil() as i32;
for dy in -r_int..=r_int {
for dx in -r_int..=r_int {
let dist_sq = (dx * dx + dy * dy) as f32;
if dist_sq > r_outer_sq {
continue;
}
let outer_edge = r_outer - dist_sq.sqrt();
let outer_alpha = outer_edge.clamp(0.0, 1.0);
let inner_edge = dist_sq.sqrt() - r_inner;
let inner_alpha = inner_edge.clamp(0.0, 1.0);
let alpha = outer_alpha * inner_alpha;
if alpha <= 0.0 {
continue;
}
let px = cx as i32 + dx;
let py = cy as i32 + dy;
if px >= 0 && (px as usize) < pb.width && py >= 0 && (py as usize) < pb.height {
let off = (py as usize * pb.width + px as usize) * 4;
for (c, &fg_val) in color.iter().enumerate().take(4) {
let bg_val = pb.data[off + c] as f32;
pb.data[off + c] = (bg_val + (fg_val as f32 - bg_val) * alpha) as u8;
}
}
}
}
let half_stroke = stroke / 2.0;
for dy in -r_int..=r_int {
for dx in -r_int..=r_int {
let dist_sq = (dx * dx + dy * dy) as f32;
if dist_sq > r_inner_sq {
continue;
}
let line_dist = ((dx as f32) - (dy as f32)).abs() / std::f32::consts::SQRT_2;
if line_dist > half_stroke + 1.0 {
continue;
}
let alpha = (half_stroke + 1.0 - line_dist).clamp(0.0, 1.0);
if alpha <= 0.0 {
continue;
}
let px = cx as i32 + dx;
let py = cy as i32 + dy;
if px >= 0 && (px as usize) < pb.width && py >= 0 && (py as usize) < pb.height {
let off = (py as usize * pb.width + px as usize) * 4;
for (c, &fg_val) in color.iter().enumerate().take(4) {
let bg_val = pb.data[off + c] as f32;
pb.data[off + c] = (bg_val + (fg_val as f32 - bg_val) * alpha) as u8;
}
}
}
}
}
fn draw_prohibit_icon(pb: &mut PixelBuffer, cx: usize, cy: usize, radius: f32) {
draw_prohibit_icon_with_stroke(pb, cx, cy, radius, 2.0);
}
fn draw_pause_icon(pb: &mut PixelBuffer, cx: usize, cy: usize, radius: f32) {
let color: [u8; 4] = [0x00, 0xC0, 0xFF, 0xFF];
let bar_h = (radius * 1.4) as i32;
let bar_w = (radius * 0.35).max(2.0) as i32;
let gap = (radius * 0.35).max(2.0) as i32;
let cx = cx as i32;
let cy = cy as i32;
let lx = cx - gap / 2 - bar_w;
let ly = cy - bar_h / 2;
let rx = cx + gap / 2;
for bar_x in [lx, rx] {
for dy in 0..bar_h {
for dx in 0..bar_w {
let px = bar_x + dx;
let py = ly + dy;
if px >= 0 && (px as usize) < pb.width && py >= 0 && (py as usize) < pb.height {
let off = (py as usize * pb.width + px as usize) * 4;
for (c, &val) in color.iter().enumerate().take(4) {
pb.data[off + c] = val;
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_badge_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
text: &str,
color: [u8; 4],
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let font_size = 24.0f32;
let (glyphs, text_w) = rasterise_glyphs(text, font, font_size);
let start_x = x0 as i32 + (w as i32 - text_w as i32) / 2;
let baseline = y0 as i32 + (h as i32 * 3) / 4;
let buf_w = pb.width;
let buf_h = pb.height;
let mut cursor_x = start_x;
for (metrics, bitmap) in &glyphs {
blit_glyph_at(
pb, metrics, bitmap, cursor_x, baseline, buf_w, buf_h, color, 1.0,
);
cursor_x += metrics.advance_width as i32;
}
}
fn render_no_sound_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let color: [u8; 4] = [0x00, 0x00, 0xFF, 0xFF]; render_badge_text(pb, font, "NO SOUND", color, x0, y0, w, h);
}
fn render_listening_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let color: [u8; 4] = [0x00, 0xC0, 0xFF, 0xFF]; render_badge_text(pb, font, "LISTENING", color, x0, y0, w, h);
}
fn render_transcribing_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let color: [u8; 4] = [0xFF, 0xCC, 0x66, 0xFF]; render_badge_text(pb, font, "TRANSCRIBING", color, x0, y0, w, h);
}
fn render_downloading_text(
pb: &mut PixelBuffer,
font: &fontdue::Font,
x0: usize,
y0: usize,
w: usize,
h: usize,
) {
let color: [u8; 4] = [0xFF, 0xCC, 0x66, 0xFF]; render_badge_text(pb, font, "DOWNLOADING MODEL", color, x0, y0, w, h);
}
const RETRY_COUNTER_FONT_SIZE: f32 = 14.0;
fn render_retry_counter(
pb: &mut PixelBuffer,
font: &fontdue::Font,
x0: usize,
y0: usize,
attempt: u32,
_max: u32,
opacity: f32,
) {
if attempt == 0 {
return;
}
let text = format!("{}", attempt);
let (glyphs, _) = rasterise_glyphs(&text, font, RETRY_COUNTER_FONT_SIZE);
let buf_w = pb.width;
let buf_h = pb.height;
let mut cursor_x = x0 as i32;
let baseline = y0 as i32 + RETRY_COUNTER_FONT_SIZE as i32;
for (metrics, bitmap) in &glyphs {
blit_glyph_at(
pb,
metrics,
bitmap,
cursor_x,
baseline,
buf_w,
buf_h,
PHASE_GREEN,
opacity,
);
cursor_x += metrics.advance_width as i32;
}
}
fn render_centered_no_sound(pb: &mut PixelBuffer, font: &fontdue::Font, bg: [u8; 4]) {
let w = pb.width;
let h = pb.height;
pb.clear_rounded(bg, CENTERED_CORNER_RADIUS);
let row1_cy = (h as f32 * 0.36) as usize;
let icon_radius = h as f32 * 0.22;
let icon_stroke = (icon_radius * 0.15).max(2.0);
let icon_diameter = (icon_radius * 2.0) as usize;
let title_color: [u8; 4] = [0x00, 0x00, 0xFF, 0xFF]; let title_size = h as f32 * 0.35;
let (title_glyphs, title_w) = rasterise_glyphs("NO SOUND", font, title_size);
let gap = (h as f32 * 0.08) as usize;
let row1_total_w = icon_diameter + gap + title_w;
let row1_start_x = (w.saturating_sub(row1_total_w)) / 2;
let icon_cx = row1_start_x + icon_radius as usize;
draw_prohibit_icon_with_stroke(pb, icon_cx, row1_cy, icon_radius, icon_stroke);
let title_x = (row1_start_x + icon_diameter + gap) as i32;
let title_baseline = row1_cy as i32 + (title_size * 0.30) as i32;
let mut cursor_x = title_x;
for (metrics, bitmap) in &title_glyphs {
blit_glyph_at(
pb,
metrics,
bitmap,
cursor_x,
title_baseline,
w,
h,
title_color,
1.0,
);
cursor_x += metrics.advance_width as i32;
}
let sub_color: [u8; 4] = [0xAA, 0xAA, 0xAA, 0xFF]; let sub_size = h as f32 * 0.18;
let sub_text = "No audio detected \u{2014} check your microphone";
let (sub_glyphs, sub_w) = rasterise_glyphs(sub_text, font, sub_size);
let sub_x = (w as i32 - sub_w as i32) / 2;
let sub_baseline = (h as i32 * 82) / 100;
let mut cursor_x = sub_x;
for (metrics, bitmap) in &sub_glyphs {
blit_glyph_at(
pb,
metrics,
bitmap,
cursor_x,
sub_baseline,
w,
h,
sub_color,
1.0,
);
cursor_x += metrics.advance_width as i32;
}
}
fn apply_alpha_shape_mask(
conn: &impl Connection,
win: u32,
img: &RgbaImage,
w: u16,
h: u16,
) -> Result<(), TalkError> {
let mask = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
conn.create_pixmap(1, mask, win, w, h)
.map_err(|e| TalkError::Config(format!("X11 create_pixmap failed: {}", e)))?;
let gc = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
conn.create_gc(gc, mask, &CreateGCAux::new().foreground(0))
.map_err(|e| TalkError::Config(format!("X11 create_gc failed: {}", e)))?;
conn.poly_fill_rectangle(
mask,
gc,
&[Rectangle {
x: 0,
y: 0,
width: w,
height: h,
}],
)
.map_err(|e| TalkError::Config(format!("X11 poly_fill_rectangle failed: {}", e)))?;
conn.change_gc(gc, &ChangeGCAux::new().foreground(1))
.map_err(|e| TalkError::Config(format!("X11 change_gc failed: {}", e)))?;
let mut opaque_points: Vec<Point> = Vec::new();
for py in 0..img.height {
for px in 0..img.width {
let idx = ((py * img.width + px) * 4) as usize;
let alpha = img.data[idx + 3];
if alpha > 128 {
opaque_points.push(Point {
x: px as i16,
y: py as i16,
});
}
}
}
for chunk in opaque_points.chunks(4096) {
conn.poly_point(CoordMode::ORIGIN, mask, gc, chunk)
.map_err(|e| TalkError::Config(format!("X11 poly_point failed: {}", e)))?;
}
shape::mask(conn, shape::SO::SET, shape::SK::BOUNDING, win, 0, 0, mask)
.map_err(|e| TalkError::Config(format!("X11 shape_mask failed: {}", e)))?;
conn.free_gc(gc)
.map_err(|e| TalkError::Config(format!("X11 free_gc failed: {}", e)))?;
conn.free_pixmap(mask)
.map_err(|e| TalkError::Config(format!("X11 free_pixmap failed: {}", e)))?;
Ok(())
}
fn draw_image(
conn: &impl Connection,
win: u32,
screen: &Screen,
img: &RgbaImage,
) -> Result<(), TalkError> {
let cmap = screen.default_colormap;
let mut color_groups: HashMap<(u8, u8, u8), Vec<Point>> = HashMap::new();
for py in 0..img.height {
for px in 0..img.width {
let idx = ((py * img.width + px) * 4) as usize;
let r = img.data[idx];
let g = img.data[idx + 1];
let b = img.data[idx + 2];
let a = img.data[idx + 3];
if a > 128 {
color_groups.entry((r, g, b)).or_default().push(Point {
x: px as i16,
y: py as i16,
});
}
}
}
let gc = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
conn.create_gc(gc, win, &CreateGCAux::new())
.map_err(|e| TalkError::Config(format!("X11 create_gc failed: {}", e)))?;
for ((r, g, b), points) in &color_groups {
let reply = conn
.alloc_color(
cmap,
(*r as u16) * 257,
(*g as u16) * 257,
(*b as u16) * 257,
)
.map_err(|e| TalkError::Config(format!("X11 alloc_color failed: {}", e)))?
.reply()
.map_err(|e| TalkError::Config(format!("X11 alloc_color reply failed: {}", e)))?;
conn.change_gc(gc, &ChangeGCAux::new().foreground(reply.pixel))
.map_err(|e| TalkError::Config(format!("X11 change_gc failed: {}", e)))?;
for chunk in points.chunks(4096) {
conn.poly_point(CoordMode::ORIGIN, win, gc, chunk)
.map_err(|e| TalkError::Config(format!("X11 poly_point failed: {}", e)))?;
}
}
conn.free_gc(gc)
.map_err(|e| TalkError::Config(format!("X11 free_gc failed: {}", e)))?;
Ok(())
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
fn overlay_thread(
rx: mpsc::Receiver<Command>,
geom: super::monitor::MonitorGeometry,
viz: Option<crate::config::VizMode>,
mono_palette: Option<([u8; 4], [u8; 4])>,
ring: Arc<Mutex<RingBuffer>>,
sample_rate: u32,
silence_tx: Option<std::sync::mpsc::Sender<bool>>,
pause_flag: Arc<std::sync::atomic::AtomicBool>,
auto_pause: bool,
mut telemetry_rx: Option<tokio::sync::broadcast::Receiver<TranscriptionEvent>>,
had_live_audio: Arc<std::sync::atomic::AtomicBool>,
) -> Result<(), TalkError> {
let (conn, screen_num) = x11rb::connect(None)
.map_err(|e| TalkError::Config(format!("failed to connect to X11: {}", e)))?;
let screen = &conn.setup().roots[screen_num];
let root = screen.root;
let argb_ctx = if let Some(visual) = find_argb_visual(screen) {
let colormap = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
conn.create_colormap(ColormapAlloc::NONE, colormap, root, visual)
.map_err(|e| TalkError::Config(format!("X11 create_colormap failed: {}", e)))?;
log::info!("using 32-bit ARGB visual for recording badge transparency");
Some(ArgbContext {
visual,
colormap,
depth: 32,
})
} else {
log::warn!("no 32-bit ARGB visual found; recording badge will have opaque background");
None
};
let depth = argb_ctx.as_ref().map_or(screen.root_depth, |c| c.depth);
let transcribing_img = decode_png(TRANSCRIBING_PNG)?;
let (mon_x, mon_y, mon_w, mon_h) = geom;
let rms_chunk: usize = sample_rate as usize / FPS as usize;
let badge_font = super::render_util::load_system_font(24.0);
let centered_h = (mon_h as f32 * CENTERED_HEIGHT_FRACTION) as u16;
let centered_h = centered_h.max(CENTERED_MIN_HEIGHT);
let centered_w = (centered_h as f32 * CENTERED_ASPECT_RATIO) as u16;
let centered_w = centered_w.min(mon_w); let centered_font = super::render_util::load_system_font(centered_h as f32 * 0.55);
let centered_bg = if argb_ctx.is_some() {
CENTERED_BG_ARGB
} else {
CENTERED_BG_OPAQUE
};
let centered_pb = if let Some(ref font) = centered_font {
let mut pb = PixelBuffer::new(centered_w as usize, centered_h as usize);
render_centered_no_sound(&mut pb, font, centered_bg);
Some(pb)
} else {
log::warn!("no font for centered no-sound overlay");
None
};
let mut centered_window: Option<u32> = None;
let mut centered_gc: Option<u32> = None;
let mut dead_signal_frames: u32 = 0;
let mut no_sound_active: bool = false;
let mut silence_notified: bool = false;
const DEAD_SIGNAL_RAIL_FLOOR: f32 = 0.9;
const DEAD_SIGNAL_FLAT_EPS: f32 = 1e-6;
const DEAD_SIGNAL_TRIGGER_FRAMES: u32 = 30;
let mut quiet_frames: u32 = 0;
let mut auto_paused: bool = false;
const AUTOPAUSE_RMS_THRESHOLD: f32 = 0.003; const AUTOPAUSE_TRIGGER_FRAMES: u32 = 15;
let mut diag_frame_counter: u32 = 0;
const DIAG_LOG_INTERVAL: u32 = 60;
let frame_dur = std::time::Duration::from_micros(1_000_000 / FPS as u64);
let mut current_window: Option<Window> = None;
let mut current_gc: Option<Gcontext> = None;
let mut is_recording = false;
let mut spectrogram_history: Vec<Vec<f32>> = Vec::new();
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
let mut rms_peak: f32 = PEAK_FLOOR;
let mut column_frame_counter: u32 = 0;
let mut current_phase: Phase = Phase::Idle;
let mut phase_history: Vec<Option<([u8; 4], f32)>> = Vec::new();
let mut current_retry: Option<(u32, u32)> = None;
let mut current_backoff: Option<(std::time::Instant, std::time::Duration)> = None;
let mut is_transcribing: bool = false;
let mut is_downloading: bool = false;
let mut current_upload_bytes: u64 = 0;
let mut prev_upload_bytes: u64 = 0;
let mut upload_peak_delta: u64 = 1;
let mut upload_history: Vec<u64> = Vec::new();
let mut current_download_bytes: u64 = 0;
let mut prev_download_bytes: u64 = 0;
let mut download_peak_delta: u64 = 1;
let mut download_history: Vec<u64> = Vec::new();
let mut current_paste_chars: u64 = 0;
let mut prev_paste_chars: u64 = 0;
let mut paste_peak_delta: u64 = 1;
let mut paste_history: Vec<u64> = Vec::new();
let mut columns_pushed_total: u64 = 0;
let amp_window_secs: f32 = 5.0;
let amp_max_frames = (FPS as f32 * amp_window_secs) as usize;
let mut amp_history: Vec<f32> = vec![0.0; amp_max_frames];
let mut amp_peak: f32 = PEAK_FLOOR;
let mut spectrum_peak: f32 = PEAK_FLOOR;
let mut spec_peak: f32 = PEAK_FLOOR;
let mut effective_freq_max: f32 = FREQ_INITIAL_MAX;
loop {
if !is_recording {
let cmd = match rx.recv() {
Ok(cmd) => cmd,
Err(_) => break,
};
match cmd {
Command::Show(IndicatorKind::Recording) => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
let badge_x = mon_x + (mon_w as i16 / 2) - (BADGE_W as i16 / 2);
let badge_y = mon_y + 4;
let win = if let Some(ref ctx) = argb_ctx {
let w = create_argb_overlay_window(
&conn, root, ctx, badge_x, badge_y, BADGE_W, BADGE_H,
)?;
apply_rounded_shape(&conn, w, BADGE_W, BADGE_H, CORNER_RADIUS)?;
w
} else {
let w = create_overlay_window(
&conn, screen, root, badge_x, badge_y, BADGE_W, BADGE_H,
)?;
apply_rounded_shape(&conn, w, BADGE_W, BADGE_H, CORNER_RADIUS)?;
w
};
conn.map_window(win)
.map_err(|e| TalkError::Config(format!("X11 map_window failed: {}", e)))?;
conn.sync()
.map_err(|e| TalkError::Config(format!("X11 sync failed: {}", e)))?;
let gc = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id: {}", e)))?;
conn.create_gc(gc, win, &CreateGCAux::new())
.map_err(|e| TalkError::Config(format!("X11 create_gc: {}", e)))?;
conn.flush()
.map_err(|e| TalkError::Config(format!("X11 flush: {}", e)))?;
current_window = Some(win);
current_gc = Some(gc);
is_recording = true;
is_transcribing = false;
is_downloading = false;
spectrogram_history.clear();
phase_history.clear();
upload_history.clear();
download_history.clear();
paste_history.clear();
current_upload_bytes = 0;
prev_upload_bytes = 0;
upload_peak_delta = 1;
current_download_bytes = 0;
prev_download_bytes = 0;
download_peak_delta = 1;
current_paste_chars = 0;
prev_paste_chars = 0;
paste_peak_delta = 1;
columns_pushed_total = 0;
current_phase = Phase::Idle;
current_retry = None;
current_backoff = None;
rms_peak = PEAK_FLOOR;
spec_peak = PEAK_FLOOR;
spectrum_peak = PEAK_FLOOR;
amp_peak = PEAK_FLOOR;
amp_history.clear();
amp_history.resize(amp_max_frames, 0.0);
effective_freq_max = FREQ_INITIAL_MAX;
dead_signal_frames = 0;
no_sound_active = false;
silence_notified = false;
quiet_frames = 0;
auto_paused = false;
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
had_live_audio.store(false, std::sync::atomic::Ordering::Relaxed);
}
Command::Show(IndicatorKind::Transcribing) => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
is_downloading = false;
show_transcribing(
&conn,
screen,
root,
&transcribing_img,
mon_x,
mon_y,
mon_w,
&mut current_window,
)?;
}
Command::Show(IndicatorKind::DownloadingModel) => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
let badge_x = mon_x + (mon_w as i16 / 2) - (BADGE_W as i16 / 2);
let badge_y = mon_y + 4;
let win = if let Some(ref ctx) = argb_ctx {
let w = create_argb_overlay_window(
&conn, root, ctx, badge_x, badge_y, BADGE_W, BADGE_H,
)?;
apply_rounded_shape(&conn, w, BADGE_W, BADGE_H, CORNER_RADIUS)?;
w
} else {
let w = create_overlay_window(
&conn, screen, root, badge_x, badge_y, BADGE_W, BADGE_H,
)?;
apply_rounded_shape(&conn, w, BADGE_W, BADGE_H, CORNER_RADIUS)?;
w
};
conn.map_window(win)
.map_err(|e| TalkError::Config(format!("X11 map_window failed: {}", e)))?;
conn.sync()
.map_err(|e| TalkError::Config(format!("X11 sync failed: {}", e)))?;
let gc = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id: {}", e)))?;
conn.create_gc(gc, win, &CreateGCAux::new())
.map_err(|e| TalkError::Config(format!("X11 create_gc: {}", e)))?;
conn.flush()
.map_err(|e| TalkError::Config(format!("X11 flush: {}", e)))?;
current_window = Some(win);
current_gc = Some(gc);
is_recording = true;
is_transcribing = false;
is_downloading = true;
spectrogram_history.clear();
phase_history.clear();
upload_history.clear();
download_history.clear();
paste_history.clear();
current_upload_bytes = 0;
prev_upload_bytes = 0;
upload_peak_delta = 1;
current_download_bytes = 0;
prev_download_bytes = 0;
download_peak_delta = 1;
current_paste_chars = 0;
prev_paste_chars = 0;
paste_peak_delta = 1;
columns_pushed_total = 0;
current_phase = Phase::Idle;
current_retry = None;
current_backoff = None;
rms_peak = PEAK_FLOOR;
spec_peak = PEAK_FLOOR;
spectrum_peak = PEAK_FLOOR;
amp_peak = PEAK_FLOOR;
amp_history.clear();
amp_history.resize(amp_max_frames, 0.0);
effective_freq_max = FREQ_INITIAL_MAX;
dead_signal_frames = 0;
no_sound_active = false;
silence_notified = false;
quiet_frames = 0;
auto_paused = false;
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
had_live_audio.store(false, std::sync::atomic::Ordering::Relaxed);
}
Command::Hide => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
}
Command::Quit => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
break;
}
}
continue;
}
let frame_start = std::time::Instant::now();
let mut quit = false;
loop {
match rx.try_recv() {
Ok(Command::Show(IndicatorKind::Recording)) => {
is_transcribing = false;
is_downloading = false;
destroy_current(&conn, &mut centered_window, &mut centered_gc);
spectrogram_history.clear();
phase_history.clear();
upload_history.clear();
download_history.clear();
paste_history.clear();
current_upload_bytes = 0;
prev_upload_bytes = 0;
upload_peak_delta = 1;
current_download_bytes = 0;
prev_download_bytes = 0;
download_peak_delta = 1;
current_paste_chars = 0;
prev_paste_chars = 0;
paste_peak_delta = 1;
columns_pushed_total = 0;
current_phase = Phase::Idle;
current_retry = None;
current_backoff = None;
rms_peak = PEAK_FLOOR;
spec_peak = PEAK_FLOOR;
spectrum_peak = PEAK_FLOOR;
amp_peak = PEAK_FLOOR;
amp_history.clear();
amp_history.resize(amp_max_frames, 0.0);
effective_freq_max = FREQ_INITIAL_MAX;
dead_signal_frames = 0;
no_sound_active = false;
silence_notified = false;
quiet_frames = 0;
auto_paused = false;
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
had_live_audio.store(false, std::sync::atomic::Ordering::Relaxed);
}
Ok(Command::Show(IndicatorKind::Transcribing)) => {
is_transcribing = true;
is_downloading = false;
destroy_current(&conn, &mut centered_window, &mut centered_gc);
}
Ok(Command::Show(IndicatorKind::DownloadingModel)) => {
is_transcribing = false;
is_downloading = true;
destroy_current(&conn, &mut centered_window, &mut centered_gc);
}
Ok(Command::Hide) => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
is_recording = false;
break;
}
Ok(Command::Quit) | Err(mpsc::TryRecvError::Disconnected) => {
destroy_current(&conn, &mut current_window, &mut current_gc);
destroy_current(&conn, &mut centered_window, &mut centered_gc);
is_recording = false;
quit = true;
break;
}
Err(mpsc::TryRecvError::Empty) => break,
}
}
if quit {
break;
}
if !is_recording {
continue;
}
if let Some(ref mut trx) = telemetry_rx {
loop {
match trx.try_recv() {
Ok(event) => {
match &event {
TranscriptionEvent::UploadProgress { bytes_sent, .. } => {
current_upload_bytes = *bytes_sent;
}
TranscriptionEvent::DownloadProgress { bytes_received, .. } => {
current_download_bytes = *bytes_received;
}
TranscriptionEvent::PasteProgress { chars_pasted, .. } => {
current_paste_chars = *chars_pasted;
}
TranscriptionEvent::RequestStarted { .. } => {
current_upload_bytes = 0;
prev_upload_bytes = 0;
current_download_bytes = 0;
prev_download_bytes = 0;
current_retry = None;
current_backoff = None;
}
TranscriptionEvent::PreflightStarted { .. } => {
current_retry = None;
current_backoff = None;
}
TranscriptionEvent::RetryScheduled {
kind,
attempt,
max,
delay,
t,
..
} => {
current_retry = Some((*attempt, *max));
current_backoff = match kind {
crate::telemetry::RetryKind::Data => Some((*t, *delay)),
crate::telemetry::RetryKind::Connection => None,
};
}
TranscriptionEvent::PasteStarted { .. } => {
current_paste_chars = 0;
prev_paste_chars = 0;
}
_ => {}
}
current_phase = current_phase.advance(&event);
}
Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
Err(tokio::sync::broadcast::error::TryRecvError::Lagged(n)) => {
log::debug!("overlay telemetry: skipped {} lagged events", n);
continue;
}
Err(tokio::sync::broadcast::error::TryRecvError::Closed) => {
telemetry_rx = None;
break;
}
}
}
}
let (frame_rms, magnitudes, frame_stuck_at_rail) = {
let samples = ring
.lock()
.map(|g| g.read_last(FFT_SIZE.max(rms_chunk)))
.unwrap_or_else(|_| vec![0.0; FFT_SIZE.max(rms_chunk)]);
let rms_slice = &samples[samples.len().saturating_sub(rms_chunk.max(1))..];
let fr = rms(rms_slice);
let stuck = is_stuck_at_rail(rms_slice, DEAD_SIGNAL_RAIL_FLOOR, DEAD_SIGNAL_FLAT_EPS);
let mags = compute_spectrum(&samples);
(fr, mags, stuck)
};
rms_peak *= PEAK_DECAY;
if frame_rms > rms_peak {
rms_peak = frame_rms;
}
rms_peak = rms_peak.max(PEAK_FLOOR);
diag_frame_counter += 1;
if diag_frame_counter >= DIAG_LOG_INTERVAL {
diag_frame_counter = 0;
log::debug!(
"[audio-diag] rms={:.6} stuck_at_rail={}",
frame_rms,
frame_stuck_at_rail,
);
}
let was_no_sound = no_sound_active;
if frame_stuck_at_rail {
dead_signal_frames = dead_signal_frames.saturating_add(1);
} else {
if no_sound_active {
if let Some(ref tx) = silence_tx {
let _ = tx.send(false);
}
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
}
dead_signal_frames = 0;
no_sound_active = false;
silence_notified = false;
}
if frame_rms >= AUTOPAUSE_RMS_THRESHOLD {
had_live_audio.store(true, std::sync::atomic::Ordering::Relaxed);
}
if dead_signal_frames >= DEAD_SIGNAL_TRIGGER_FRAMES {
no_sound_active = true;
if !silence_notified {
if let Some(ref tx) = silence_tx {
let _ = tx.send(true);
}
pause_flag.store(true, std::sync::atomic::Ordering::Relaxed);
silence_notified = true;
}
}
if no_sound_active && !was_no_sound {
if let Some(ref cpb) = centered_pb {
if centered_window.is_none() {
let cx = mon_x + (mon_w as i16 / 2) - (centered_w as i16 / 2);
let cy = mon_y + (mon_h as i16 / 2) - (centered_h as i16 / 2);
let win = if let Some(ref ctx) = argb_ctx {
create_argb_overlay_window(&conn, root, ctx, cx, cy, centered_w, centered_h)
} else {
create_overlay_window(&conn, screen, root, cx, cy, centered_w, centered_h)
};
if let Ok(w) = win {
let _ = apply_rounded_shape(
&conn,
w,
centered_w,
centered_h,
CENTERED_CORNER_RADIUS,
);
let _ = conn.map_window(w);
let _ = conn.sync();
if let Ok(gc) = conn.generate_id() {
let _ = conn.create_gc(gc, w, &CreateGCAux::new());
let _ = conn.put_image(
ImageFormat::Z_PIXMAP,
w,
gc,
centered_w,
centered_h,
0,
0,
0,
depth,
&cpb.data,
);
let _ = conn.flush();
centered_window = Some(w);
centered_gc = Some(gc);
}
}
}
}
} else if !no_sound_active && was_no_sound {
destroy_current(&conn, &mut centered_window, &mut centered_gc);
}
if auto_pause && !no_sound_active {
if frame_rms < AUTOPAUSE_RMS_THRESHOLD {
quiet_frames = quiet_frames.saturating_add(1);
} else {
quiet_frames = 0;
if auto_paused {
auto_paused = false;
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
log::debug!("auto-pause: resumed (speech detected)");
}
}
if quiet_frames >= AUTOPAUSE_TRIGGER_FRAMES && !auto_paused {
auto_paused = true;
pause_flag.store(true, std::sync::atomic::Ordering::Relaxed);
log::debug!("auto-pause: paused (silence detected)");
}
} else if !auto_pause {
quiet_frames = 0;
} else {
quiet_frames = 0;
if auto_paused {
auto_paused = false;
pause_flag.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
if !auto_paused && !no_sound_active {
if let Some(mode) = viz {
use crate::config::VizMode;
match mode {
VizMode::Waterfall => {
let nyquist = sample_rate as f32 / 2.0;
let n_mag = magnitudes.len();
for (i, &mag) in magnitudes.iter().enumerate().rev() {
if mag > FREQ_NOISE_FLOOR {
let freq = (i as f32 / n_mag as f32) * nyquist;
if freq > effective_freq_max {
effective_freq_max = freq.min(FREQ_MAX);
}
break;
}
}
let frame_spec_max = magnitudes.iter().copied().fold(0.0f32, f32::max);
if frame_spec_max > spec_peak {
spec_peak = frame_spec_max;
}
}
VizMode::Amplitude => {
if frame_rms > amp_peak {
amp_peak = frame_rms;
}
}
VizMode::Spectrum => {
spectrum_peak *= PEAK_DECAY;
let frame_peak = magnitudes.iter().copied().fold(0.0f32, f32::max);
if frame_peak > spectrum_peak {
spectrum_peak = frame_peak;
}
spectrum_peak = spectrum_peak.max(PEAK_FLOOR);
}
}
}
}
column_frame_counter = column_frame_counter.wrapping_add(1);
if column_frame_counter.is_multiple_of(COLUMN_PERIOD_FRAMES)
&& (!no_sound_active || is_transcribing || is_downloading)
{
if let Some(mode) = viz {
use crate::config::VizMode;
match mode {
VizMode::Waterfall => {
let column = if auto_paused || is_transcribing || is_downloading {
vec![0.0f32; SPEC_H]
} else {
map_spectrum_to_column(
&magnitudes,
SPEC_H,
sample_rate,
effective_freq_max,
)
};
spectrogram_history.push(column);
if spectrogram_history.len() > SPEC_W {
spectrogram_history.drain(..spectrogram_history.len() - SPEC_W);
}
}
VizMode::Amplitude => {
let val = if auto_paused || is_transcribing || is_downloading {
0.0
} else {
frame_rms
};
amp_history.push(val);
if amp_history.len() > amp_max_frames {
amp_history.drain(..amp_history.len() - amp_max_frames);
}
}
VizMode::Spectrum => {
}
}
}
columns_pushed_total = columns_pushed_total.wrapping_add(1);
phase_history.push(current_phase.color());
if phase_history.len() > SPEC_W {
phase_history.drain(..phase_history.len() - SPEC_W);
}
let up_delta = current_upload_bytes.saturating_sub(prev_upload_bytes);
prev_upload_bytes = current_upload_bytes;
if up_delta > upload_peak_delta {
upload_peak_delta = up_delta;
}
upload_history.push(up_delta);
if upload_history.len() > SPEC_W {
upload_history.drain(..upload_history.len() - SPEC_W);
}
let dl_delta = current_download_bytes.saturating_sub(prev_download_bytes);
prev_download_bytes = current_download_bytes;
if dl_delta > download_peak_delta {
download_peak_delta = dl_delta;
}
download_history.push(dl_delta);
if download_history.len() > SPEC_W {
download_history.drain(..download_history.len() - SPEC_W);
}
let paste_delta = current_paste_chars.saturating_sub(prev_paste_chars);
prev_paste_chars = current_paste_chars;
if paste_delta > paste_peak_delta {
paste_peak_delta = paste_delta;
}
paste_history.push(paste_delta);
if paste_history.len() > SPEC_W {
paste_history.drain(..paste_history.len() - SPEC_W);
}
}
pb.clear(BG_COLOR);
draw_rounded_border(&mut pb, BORDER_COLOR, CORNER_RADIUS as f32, BORDER_WIDTH);
if is_transcribing || is_downloading {
if let Some(mode) = viz {
use crate::config::VizMode;
match mode {
VizMode::Waterfall => {
render_spectrogram(
&mut pb,
&spectrogram_history,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
spec_peak,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
VizMode::Amplitude => {
render_amplitude_badge(
&mut pb,
&_history,
amp_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
VizMode::Spectrum => {
render_spectrum_badge(
&mut pb,
&magnitudes,
spectrum_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
}
}
render_time_grid(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
columns_pushed_total.saturating_sub(spectrogram_history.len() as u64),
spectrogram_history.len(),
COLUMNS_PER_GRID_MARK,
);
render_phase_line(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, &phase_history);
if current_phase == Phase::WaitingRetry {
if let Some((started, delay)) = current_backoff {
let fraction =
backoff_remaining_fraction(std::time::Instant::now(), started, delay);
render_backoff_bar(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, fraction);
}
}
render_throughput_tracks(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
&upload_history,
&download_history,
&paste_history,
&phase_history,
upload_peak_delta,
download_peak_delta,
paste_peak_delta,
);
if let Some(ref f) = badge_font {
if is_downloading {
render_downloading_text(&mut pb, f, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H);
} else if let (Phase::WaitingRetry, Some((attempt, max))) =
(current_phase, current_retry)
{
render_backoff_text(
&mut pb,
f,
(attempt, max),
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
);
} else {
render_transcribing_text(&mut pb, f, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H);
if let Some((attempt, max)) = current_retry {
render_retry_counter(
&mut pb,
f,
SPEC_LEFT + 2,
SPEC_TOP + PHASE_LINE_HEIGHT,
attempt,
max,
current_phase.opacity(),
);
}
}
}
} else if no_sound_active {
if let Some(ref f) = badge_font {
render_no_sound_text(&mut pb, f, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H);
}
clear_dot_gap(&mut pb, DOT_CX, DOT_CY, DOT_RADIUS_MAX + DOT_GAP);
draw_prohibit_icon(&mut pb, DOT_CX, DOT_CY, PROHIBIT_ICON_RADIUS);
} else if auto_paused {
if let Some(mode) = viz {
use crate::config::VizMode;
match mode {
VizMode::Waterfall => {
render_spectrogram(
&mut pb,
&spectrogram_history,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
spec_peak,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
VizMode::Amplitude => {
render_amplitude_badge(
&mut pb,
&_history,
amp_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
VizMode::Spectrum => {
render_spectrum_badge(
&mut pb,
&magnitudes,
spectrum_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
DIM_FACTOR_PAUSED,
);
}
}
}
render_time_grid(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
columns_pushed_total.saturating_sub(spectrogram_history.len() as u64),
spectrogram_history.len(),
COLUMNS_PER_GRID_MARK,
);
render_phase_line(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, &phase_history);
if current_phase == Phase::WaitingRetry {
if let Some((started, delay)) = current_backoff {
let fraction =
backoff_remaining_fraction(std::time::Instant::now(), started, delay);
render_backoff_bar(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, fraction);
}
}
render_throughput_tracks(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
&upload_history,
&download_history,
&paste_history,
&phase_history,
upload_peak_delta,
download_peak_delta,
paste_peak_delta,
);
if let Some(ref f) = badge_font {
render_listening_text(&mut pb, f, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H);
}
clear_dot_gap(&mut pb, DOT_CX, DOT_CY, DOT_RADIUS_MAX + DOT_GAP);
draw_pause_icon(&mut pb, DOT_CX, DOT_CY, DOT_RADIUS_MAX);
} else {
if let Some(mode) = viz {
use crate::config::VizMode;
match mode {
VizMode::Waterfall => {
render_spectrogram(
&mut pb,
&spectrogram_history,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
spec_peak,
mono_palette,
1.0,
);
}
VizMode::Amplitude => {
render_amplitude_badge(
&mut pb,
&_history,
amp_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
1.0,
);
}
VizMode::Spectrum => {
render_spectrum_badge(
&mut pb,
&magnitudes,
spectrum_peak,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
mono_palette,
1.0,
);
}
}
}
render_time_grid(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
columns_pushed_total.saturating_sub(spectrogram_history.len() as u64),
spectrogram_history.len(),
COLUMNS_PER_GRID_MARK,
);
render_phase_line(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, &phase_history);
if current_phase == Phase::WaitingRetry {
if let Some((started, delay)) = current_backoff {
let fraction =
backoff_remaining_fraction(std::time::Instant::now(), started, delay);
render_backoff_bar(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, fraction);
}
}
render_throughput_tracks(
&mut pb,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
&upload_history,
&download_history,
&paste_history,
&phase_history,
upload_peak_delta,
download_peak_delta,
paste_peak_delta,
);
let vol_norm = if rms_peak > PEAK_FLOOR {
(frame_rms / rms_peak).clamp(0.0, 1.0)
} else {
0.0
};
let dot_radius = DOT_RADIUS_MIN + (DOT_RADIUS_MAX - DOT_RADIUS_MIN) * vol_norm;
let dot_brightness = DOT_MIN_BRIGHTNESS + (1.0 - DOT_MIN_BRIGHTNESS) * vol_norm;
clear_dot_gap(&mut pb, DOT_CX, DOT_CY, dot_radius + DOT_GAP);
draw_pulsing_dot(&mut pb, DOT_CX, DOT_CY, dot_radius, dot_brightness);
}
if let (Some(win), Some(gc)) = (current_window, current_gc) {
let _ = conn.put_image(
ImageFormat::Z_PIXMAP,
win,
gc,
BADGE_W,
BADGE_H,
0,
0,
0,
depth,
&pb.data,
);
let _ = conn.flush();
}
let elapsed = frame_start.elapsed();
if elapsed < frame_dur {
std::thread::sleep(frame_dur - elapsed);
}
}
Ok(())
}
fn find_argb_visual(screen: &Screen) -> Option<Visualid> {
screen
.allowed_depths
.iter()
.filter(|d| d.depth == 32)
.flat_map(|d| &d.visuals)
.find(|v| v.class == VisualClass::TRUE_COLOR)
.map(|v| v.visual_id)
}
struct ArgbContext {
visual: Visualid,
colormap: Colormap,
depth: u8,
}
fn create_argb_overlay_window(
conn: &impl Connection,
root: u32,
ctx: &ArgbContext,
x: i16,
y: i16,
w: u16,
h: u16,
) -> Result<u32, TalkError> {
let win = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
let values = CreateWindowAux::new()
.background_pixel(0) .border_pixel(0) .override_redirect(1u32)
.event_mask(EventMask::EXPOSURE)
.colormap(ctx.colormap);
conn.create_window(
ctx.depth,
win,
root,
x,
y,
w,
h,
0,
WindowClass::INPUT_OUTPUT,
ctx.visual,
&values,
)
.map_err(|e| TalkError::Config(format!("X11 create_window failed: {}", e)))?;
Ok(win)
}
fn create_overlay_window(
conn: &impl Connection,
screen: &Screen,
root: u32,
x: i16,
y: i16,
w: u16,
h: u16,
) -> Result<u32, TalkError> {
let win = conn
.generate_id()
.map_err(|e| TalkError::Config(format!("X11 generate_id failed: {}", e)))?;
let values = CreateWindowAux::new()
.background_pixel(screen.black_pixel)
.border_pixel(0)
.override_redirect(1u32)
.event_mask(EventMask::EXPOSURE);
conn.create_window(
COPY_DEPTH_FROM_PARENT,
win,
root,
x,
y,
w,
h,
0,
WindowClass::INPUT_OUTPUT,
0,
&values,
)
.map_err(|e| TalkError::Config(format!("X11 create_window failed: {}", e)))?;
Ok(win)
}
#[allow(clippy::too_many_arguments)]
fn show_transcribing(
conn: &impl Connection,
screen: &Screen,
root: u32,
img: &RgbaImage,
mon_x: i16,
mon_y: i16,
mon_w: u16,
current_window: &mut Option<u32>,
) -> Result<(), TalkError> {
let w = img.width as u16;
let h = img.height as u16;
let x = mon_x + (mon_w as i16 / 2) - (w as i16 / 2);
let y = mon_y + 4;
let win = create_overlay_window(conn, screen, root, x, y, w, h)?;
apply_alpha_shape_mask(conn, win, img, w, h)?;
conn.map_window(win)
.map_err(|e| TalkError::Config(format!("X11 map_window failed: {}", e)))?;
conn.sync()
.map_err(|e| TalkError::Config(format!("X11 sync failed: {}", e)))?;
draw_image(conn, win, screen, img)?;
conn.flush()
.map_err(|e| TalkError::Config(format!("X11 flush failed: {}", e)))?;
*current_window = Some(win);
Ok(())
}
fn destroy_current(conn: &impl Connection, window: &mut Option<u32>, gc: &mut Option<u32>) {
if let Some(g) = gc.take() {
let _ = conn.free_gc(g);
}
if let Some(win) = window.take() {
let _ = conn.destroy_window(win);
let _ = conn.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
const RAIL_FLOOR: f32 = 0.9;
const FLAT_EPS: f32 = 1e-6;
#[test]
fn stuck_at_rail_detects_disconnected_device() {
let frame = vec![-1.0f32; 266];
assert!(is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_detects_positive_rail() {
let frame = vec![1.0f32; 266];
assert!(is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_ignores_zero_silence() {
let frame = vec![0.0f32; 266];
assert!(!is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_ignores_quiet_noise_floor() {
let frame: Vec<f32> = (0..266)
.map(|i| if i % 2 == 0 { 8.0 } else { -10.0 } / 32768.0)
.collect();
assert!(!is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_ignores_loud_speech() {
let frame: Vec<f32> = (0..266)
.map(|i| if i % 3 == 0 { 1.0 } else { -0.5 })
.collect();
assert!(!is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_ignores_constant_midlevel() {
let frame = vec![0.5f32; 266];
assert!(!is_stuck_at_rail(&frame, RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn stuck_at_rail_empty_is_not_dead() {
assert!(!is_stuck_at_rail(&[], RAIL_FLOOR, FLAT_EPS));
}
#[test]
fn test_decode_transcribing_png() {
let img = decode_png(TRANSCRIBING_PNG).expect("decode transcribing PNG");
assert_eq!(img.width, 210);
assert_eq!(img.height, 52);
assert_eq!(img.data.len(), 210 * 52 * 4);
}
#[test]
fn test_transcribing_png_has_opaque_pixels() {
let img = decode_png(TRANSCRIBING_PNG).expect("decode");
let opaque_count = (0..img.width * img.height)
.filter(|&i| img.data[(i * 4 + 3) as usize] > 128)
.count();
assert!(
opaque_count > 1000,
"expected >1000 opaque pixels, got {}",
opaque_count
);
}
#[test]
fn map_spectrum_column_length() {
let magnitudes = vec![1.0f32; 512];
let column = map_spectrum_to_column(&magnitudes, SPEC_H, 48000, FREQ_MAX);
assert_eq!(column.len(), SPEC_H);
}
#[test]
fn map_spectrum_empty_magnitudes() {
let column = map_spectrum_to_column(&[], SPEC_H, 48000, FREQ_MAX);
assert_eq!(column.len(), SPEC_H);
assert!(column.iter().all(|&v| v == 0.0));
}
#[test]
fn map_spectrum_low_freq_comes_first() {
let mut magnitudes = vec![0.0f32; 512];
for m in magnitudes.iter_mut().take(10) {
*m = 10.0;
}
let column = map_spectrum_to_column(&magnitudes, SPEC_H, 48000, FREQ_MAX);
assert!(
column[0] > column[SPEC_H - 1],
"low freq row ({}) should be louder than high freq row ({})",
column[0],
column[SPEC_H - 1]
);
}
#[test]
fn render_spectrogram_no_panic() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
let history: Vec<Vec<f32>> = (0..SPEC_W)
.map(|i| vec![(i as f32 * 0.01).sin().abs(); SPEC_H])
.collect();
render_spectrogram(
&mut pb, &history, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 1.0, None, 1.0,
);
let mut non_bg = 0;
for y in SPEC_TOP..SPEC_BOTTOM {
for x in SPEC_LEFT..SPEC_RIGHT {
let off = (y * pb.width + x) * 4;
if pb.data[off..off + 4] != BG_COLOR {
non_bg += 1;
}
}
}
assert!(non_bg > 0, "spectrogram should produce non-bg pixels");
}
#[test]
fn render_spectrogram_empty_history_no_panic() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
render_spectrogram(
&mut pb,
&[],
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
1.0,
None,
1.0,
);
}
#[test]
fn render_time_grid_draws_dotted_yellow_at_first_mark_on_blank_buffer() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
render_time_grid(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 0, 65, 30);
let first_x = SPEC_LEFT + SPEC_W - 65;
let off = (SPEC_TOP * pb.width + first_x) * 4;
assert_eq!(pb.data[off], 0, "grid blue channel unchanged by yellow");
assert!(
pb.data[off + 1] > 0 && pb.data[off + 1] < 200,
"grid green should show blended yellow, got {}",
pb.data[off + 1]
);
assert!(
pb.data[off + 2] > 0 && pb.data[off + 2] < 200,
"grid red should show blended yellow, got {}",
pb.data[off + 2]
);
let off_gap = ((SPEC_TOP + 1) * pb.width + first_x) * 4;
assert_eq!(
&pb.data[off_gap..off_gap + 4],
&BG_COLOR,
"row 1 of grid column should be untouched (dot gap)"
);
let off_dot2 = ((SPEC_TOP + 2) * pb.width + first_x) * 4;
assert!(
pb.data[off_dot2 + 1] > 0 && pb.data[off_dot2 + 2] > 0,
"row 2 of grid column should be another dot"
);
let non_grid_x = SPEC_LEFT + SPEC_W - 65 + 5;
let off_ng = (SPEC_TOP * pb.width + non_grid_x) * 4;
assert_eq!(
&pb.data[off_ng..off_ng + 4],
&BG_COLOR,
"non-grid column should remain background"
);
}
#[test]
fn render_time_grid_blends_with_existing_waterfall_pixels() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
for y in SPEC_TOP..SPEC_BOTTOM {
for x in SPEC_LEFT..SPEC_RIGHT {
pb.set_pixel(x, y, [255, 255, 255, 255]);
}
}
render_time_grid(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 0, 1, 30);
let x = SPEC_LEFT + SPEC_W - 1;
let off = (SPEC_TOP * pb.width + x) * 4;
assert!(
pb.data[off] < 200,
"grid dot blue should drop below white (got {})",
pb.data[off]
);
assert!(pb.data[off + 1] >= 250, "green should stay high");
assert!(pb.data[off + 2] >= 250, "red should stay high");
}
#[test]
fn render_time_grid_no_marks_when_fewer_than_one_period_visible() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
render_time_grid(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 1, 5, 30);
for y in SPEC_TOP..SPEC_BOTTOM {
for x in SPEC_LEFT..SPEC_RIGHT {
let off = (y * pb.width + x) * 4;
assert_eq!(
&pb.data[off..off + 4],
&BG_COLOR,
"no marks should have been drawn at ({},{})",
x,
y
);
}
}
}
#[test]
fn render_time_grid_noop_for_zero_visible_columns() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
render_time_grid(&mut pb, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 0, 0, 30);
for y in SPEC_TOP..SPEC_BOTTOM {
for x in SPEC_LEFT..SPEC_RIGHT {
let off = (y * pb.width + x) * 4;
assert_eq!(&pb.data[off..off + 4], &BG_COLOR);
}
}
}
#[test]
fn render_spectrogram_dim_produces_darker_pixels_than_full() {
let history: Vec<Vec<f32>> = (0..SPEC_W).map(|_| vec![1.0; SPEC_H]).collect();
let mut pb_full = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb_full.clear(BG_COLOR);
render_spectrogram(
&mut pb_full,
&history,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
1.0,
None,
1.0,
);
let mut pb_dim = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb_dim.clear(BG_COLOR);
render_spectrogram(
&mut pb_dim,
&history,
SPEC_LEFT,
SPEC_TOP,
SPEC_W,
SPEC_H,
1.0,
None,
DIM_FACTOR_PAUSED,
);
let sample_x = SPEC_LEFT + SPEC_W / 2;
let sample_y = SPEC_TOP + SPEC_H / 2;
let off = (sample_y * pb_full.width + sample_x) * 4;
let full_intensity =
pb_full.data[off] as u32 + pb_full.data[off + 1] as u32 + pb_full.data[off + 2] as u32;
let dim_intensity =
pb_dim.data[off] as u32 + pb_dim.data[off + 1] as u32 + pb_dim.data[off + 2] as u32;
assert!(
full_intensity > 0,
"full-bright spectrogram pixel should have non-zero intensity"
);
assert!(
dim_intensity < full_intensity,
"dim ({}) must be strictly less than full ({})",
dim_intensity,
full_intensity
);
}
#[test]
fn render_spectrogram_zero_column_produces_no_pixels() {
let history: Vec<Vec<f32>> = vec![vec![0.0f32; SPEC_H]; SPEC_W];
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
render_spectrogram(
&mut pb, &history, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 1.0, None, 1.0,
);
let mut non_bg = 0;
for y in SPEC_TOP..SPEC_BOTTOM {
for x in SPEC_LEFT..SPEC_RIGHT {
let off = (y * pb.width + x) * 4;
if pb.data[off..off + 4] != BG_COLOR {
non_bg += 1;
}
}
}
assert_eq!(
non_bg, 0,
"all-zero columns should produce no non-background pixels (got {})",
non_bg
);
}
#[test]
fn render_spectrogram_right_aligned() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
let history: Vec<Vec<f32>> = (0..5).map(|_| vec![1.0; SPEC_H]).collect();
render_spectrogram(
&mut pb, &history, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 1.0, None, 1.0,
);
let check_col = SPEC_LEFT;
let mid_y = SPEC_TOP + SPEC_H / 2;
let off = (mid_y * pb.width + check_col) * 4;
assert_eq!(
&pb.data[off..off + 4],
&BG_COLOR,
"leftmost spectrogram column should remain background when history is short"
);
let right_col = SPEC_LEFT + SPEC_W - 1; let off2 = (mid_y * pb.width + right_col) * 4;
assert!(
pb.data[off2 + 3] > 0,
"rightmost spectrogram column should have non-zero alpha"
);
}
#[test]
fn draw_pulsing_dot_no_panic() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
draw_pulsing_dot(&mut pb, DOT_CX, DOT_CY, DOT_RADIUS_MAX, 0.8);
let off = (DOT_CY * pb.width + DOT_CX) * 4;
assert!(
pb.data[off + 2] > 0,
"dot centre red channel should be non-zero"
);
}
#[test]
fn draw_pulsing_dot_dim_vs_bright() {
let mut pb_dim = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb_dim.clear(BG_COLOR);
draw_pulsing_dot(
&mut pb_dim,
DOT_CX,
DOT_CY,
DOT_RADIUS_MAX,
DOT_MIN_BRIGHTNESS,
);
let off = (DOT_CY * pb_dim.width + DOT_CX) * 4;
let dim_r = pb_dim.data[off + 2];
let mut pb_bright = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb_bright.clear(BG_COLOR);
draw_pulsing_dot(&mut pb_bright, DOT_CX, DOT_CY, DOT_RADIUS_MAX, 1.0);
let bright_r = pb_bright.data[off + 2];
assert!(
bright_r > dim_r,
"bright dot ({}) should have higher red than dim ({})",
bright_r,
dim_r
);
}
#[test]
fn spectrogram_area_fits_in_badge() {
let bw = BADGE_W as usize;
let bh = BADGE_H as usize;
assert!(
SPEC_RIGHT <= bw,
"spectrogram right edge exceeds badge width"
);
assert!(
SPEC_BOTTOM <= bh,
"spectrogram bottom edge exceeds badge height"
);
}
#[test]
fn dot_fits_in_badge() {
let r = DOT_RADIUS_MAX.ceil() as usize;
assert!(DOT_CX >= r);
assert!(DOT_CY >= r);
assert!(DOT_CX + r < BADGE_W as usize);
assert!(DOT_CY + r < BADGE_H as usize);
}
#[test]
fn indicator_kind_clone_eq() {
let a = IndicatorKind::Recording;
let b = a;
assert_eq!(a, b);
assert_ne!(IndicatorKind::Recording, IndicatorKind::Transcribing);
assert_ne!(IndicatorKind::Recording, IndicatorKind::DownloadingModel);
assert_ne!(IndicatorKind::Transcribing, IndicatorKind::DownloadingModel);
let c = IndicatorKind::DownloadingModel;
let d = c;
assert_eq!(c, d);
}
#[test]
fn rounded_rect_sdf_centre_is_negative() {
let w = BADGE_W as f32;
let h = BADGE_H as f32;
let d = rounded_rect_sdf(w / 2.0, h / 2.0, w, h, CORNER_RADIUS as f32);
assert!(
d < 0.0,
"centre of badge should be inside (negative SDF), got {}",
d
);
}
#[test]
fn rounded_rect_sdf_outside_is_positive() {
let w = BADGE_W as f32;
let h = BADGE_H as f32;
let d = rounded_rect_sdf(w + 10.0, h + 10.0, w, h, CORNER_RADIUS as f32);
assert!(
d > 0.0,
"point outside badge should have positive SDF, got {}",
d
);
}
#[test]
fn draw_rounded_border_produces_border_pixels() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
draw_rounded_border(&mut pb, BORDER_COLOR, CORNER_RADIUS as f32, BORDER_WIDTH);
let mid_x = BADGE_W as usize / 2;
let off = mid_x * 4;
assert!(
pb.data[off + 3] > 0,
"top edge centre should have non-zero alpha from border"
);
let cx = BADGE_W as usize / 2;
let cy = BADGE_H as usize / 2;
let off_centre = (cy * pb.width + cx) * 4;
assert_eq!(
&pb.data[off_centre..off_centre + 4],
&BG_COLOR,
"badge interior should remain at background color"
);
}
#[test]
fn full_badge_render_produces_content() {
let mut pb = PixelBuffer::new(BADGE_W as usize, BADGE_H as usize);
pb.clear(BG_COLOR);
draw_rounded_border(&mut pb, BORDER_COLOR, CORNER_RADIUS as f32, BORDER_WIDTH);
draw_pulsing_dot(&mut pb, DOT_CX, DOT_CY, DOT_RADIUS_MAX, 0.7);
let history: Vec<Vec<f32>> = (0..SPEC_W)
.map(|i| vec![(i as f32 * 0.05).sin().abs(); SPEC_H])
.collect();
render_spectrogram(
&mut pb, &history, SPEC_LEFT, SPEC_TOP, SPEC_W, SPEC_H, 1.0, None, 1.0,
);
let visible = pb.data.chunks_exact(4).filter(|p| p[3] > 0).count();
assert!(
visible > 500,
"full badge render should produce significant visible content, got {} pixels with alpha > 0",
visible
);
}
#[test]
fn phase_advance_preflight_started_enters_validating() {
use std::time::Instant;
let now = Instant::now();
let after = Phase::Idle.advance(&TranscriptionEvent::PreflightStarted { t: now });
assert_eq!(after, Phase::Validating);
}
#[test]
fn phase_advance_preflight_completed_success_returns_to_idle() {
use std::time::Instant;
let after = Phase::Validating.advance(&TranscriptionEvent::PreflightCompleted {
success: true,
t: Instant::now(),
});
assert_eq!(after, Phase::Idle);
}
#[test]
fn phase_advance_preflight_completed_failure_enters_error() {
use std::time::Instant;
let after = Phase::Validating.advance(&TranscriptionEvent::PreflightCompleted {
success: false,
t: Instant::now(),
});
assert_eq!(after, Phase::Error);
}
#[test]
fn phase_advance_retry_during_validating_stays_validating() {
use std::time::Instant;
let after = Phase::Validating.advance(&TranscriptionEvent::RetryScheduled {
kind: crate::telemetry::RetryKind::Connection,
attempt: 2,
max: 5,
reason: "timeout".into(),
delay: std::time::Duration::ZERO,
t: Instant::now(),
});
assert_eq!(after, Phase::Validating);
}
#[test]
fn phase_advance_retry_outside_validating_returns_to_connecting() {
use std::time::Instant;
let after = Phase::Receiving.advance(&TranscriptionEvent::RetryScheduled {
kind: crate::telemetry::RetryKind::Connection,
attempt: 2,
max: 5,
reason: "timeout".into(),
delay: std::time::Duration::ZERO,
t: Instant::now(),
});
assert_eq!(after, Phase::Connecting);
}
#[test]
fn phase_color_validating_is_green_at_half_opacity() {
let (color, opacity) = Phase::Validating.color().expect("validating has a color");
assert_eq!(color, PHASE_GREEN);
assert!(
(opacity - VALIDATING_OPACITY).abs() < f32::EPSILON,
"expected {}, got {}",
VALIDATING_OPACITY,
opacity
);
}
#[test]
fn phase_color_done_is_green_at_full_opacity() {
let (color, opacity) = Phase::Done.color().expect("done has a color");
assert_eq!(color, PHASE_GREEN);
assert!((opacity - 1.0).abs() < f32::EPSILON);
}
#[test]
fn phase_color_idle_is_none() {
assert!(Phase::Idle.color().is_none());
}
#[test]
fn phase_color_all_non_validating_phases_render_at_full_opacity() {
for phase in [
Phase::Connecting,
Phase::Uploading,
Phase::WaitingResponse,
Phase::Receiving,
Phase::Done,
Phase::Error,
] {
let (_, opacity) = phase.color().expect("non-idle phase has color");
assert!(
(opacity - 1.0).abs() < f32::EPSILON,
"phase {:?} should render at full opacity, got {}",
phase,
opacity
);
}
}
use std::time::{Duration, Instant};
fn pixel(pb: &PixelBuffer, x: usize, y: usize) -> [u8; 4] {
let off = (y * pb.width + x) * 4;
[
pb.data[off],
pb.data[off + 1],
pb.data[off + 2],
pb.data[off + 3],
]
}
fn data_retry(delay_secs: u64) -> TranscriptionEvent {
TranscriptionEvent::RetryScheduled {
kind: crate::telemetry::RetryKind::Data,
attempt: 3,
max: 6,
reason: "503 high load".into(),
delay: Duration::from_secs(delay_secs),
t: Instant::now(),
}
}
#[test]
fn phase_advance_data_retry_enters_waiting_retry() {
for from in [
Phase::Connecting,
Phase::Uploading,
Phase::WaitingResponse,
Phase::Receiving,
] {
assert_eq!(
from.advance(&data_retry(30)),
Phase::WaitingRetry,
"from {:?}",
from
);
}
let conn = TranscriptionEvent::RetryScheduled {
kind: crate::telemetry::RetryKind::Connection,
attempt: 2,
max: 6,
reason: "timeout".into(),
delay: Duration::ZERO,
t: Instant::now(),
};
assert_eq!(Phase::Receiving.advance(&conn), Phase::Connecting);
}
#[test]
fn phase_advance_data_retry_during_validating_stays_validating() {
assert_eq!(
Phase::Validating.advance(&data_retry(30)),
Phase::Validating
);
}
#[test]
fn phase_advance_waiting_retry_returns_to_connecting_on_request_started() {
let after = Phase::WaitingRetry.advance(&TranscriptionEvent::RequestStarted {
endpoint: "https://x".into(),
t: Instant::now(),
});
assert_eq!(after, Phase::Connecting);
}
#[test]
fn phase_color_waiting_retry_is_dim_amber() {
let (color, opacity) = Phase::WaitingRetry.color().expect("has colour");
assert_eq!(color, PHASE_AMBER);
assert!((opacity - BACKOFF_TRAIL_OPACITY).abs() < f32::EPSILON);
assert!(opacity < 1.0 && opacity > 0.0);
for other in [
Phase::Connecting,
Phase::Uploading,
Phase::Receiving,
Phase::Done,
Phase::Error,
] {
let (c, _) = other.color().expect("has colour");
assert_ne!(
c, PHASE_AMBER,
"{:?} must not share the backoff amber",
other
);
}
}
#[test]
fn backoff_remaining_fraction_drains_linearly_and_clamps() {
let started = Instant::now();
let delay = Duration::from_secs(40);
let at = |secs: u64| {
backoff_remaining_fraction(started + Duration::from_secs(secs), started, delay)
};
assert!((at(0) - 1.0).abs() < 1e-6);
assert!((at(20) - 0.5).abs() < 1e-6);
assert!((at(40) - 0.0).abs() < 1e-6);
assert!((at(90) - 0.0).abs() < 1e-6, "must clamp after the delay");
}
#[test]
fn backoff_remaining_fraction_zero_delay_is_empty() {
let started = Instant::now();
assert!((backoff_remaining_fraction(started, started, Duration::ZERO) - 0.0).abs() < 1e-6);
}
#[test]
fn render_backoff_bar_fills_left_portion_proportionally() {
let mut pb = PixelBuffer::new(120, 20);
render_backoff_bar(&mut pb, 10, 5, 100, 0.5);
assert_eq!(pixel(&pb, 10, 5), PHASE_AMBER, "left edge amber");
assert_eq!(pixel(&pb, 59, 6), PHASE_AMBER, "last filled column amber");
assert_eq!(
pixel(&pb, 60, 5),
[0, 0, 0, 0],
"first drained column untouched"
);
assert_eq!(pixel(&pb, 109, 5), [0, 0, 0, 0], "right edge untouched");
assert_eq!(
pixel(&pb, 10, 5 + PHASE_LINE_HEIGHT),
[0, 0, 0, 0],
"below the band untouched"
);
}
#[test]
fn backoff_badge_label_names_cause_and_progress() {
assert_eq!(backoff_badge_label(3, 6), "BUSY · RETRY 3/6");
}
#[test]
fn phase_opacity_idle_is_full() {
assert!((Phase::Idle.opacity() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn phase_opacity_validating_is_half() {
assert!((Phase::Validating.opacity() - VALIDATING_OPACITY).abs() < f32::EPSILON);
}
#[test]
fn retry_counter_attempt_zero_writes_no_pixels() {
let font = match decode_font_or_skip() {
Some(f) => f,
None => return,
};
let mut pb = PixelBuffer::new(SPEC_W, SPEC_H);
pb.clear([0, 0, 0, 0]);
render_retry_counter(&mut pb, &font, 2, 4, 0, 5, 1.0);
let any_painted = pb.data.iter().any(|&b| b != 0);
assert!(!any_painted, "attempt=0 must not paint anything");
}
#[test]
fn retry_counter_attempt_one_writes_visible_pixels() {
let font = match decode_font_or_skip() {
Some(f) => f,
None => return,
};
let mut pb = PixelBuffer::new(SPEC_W, SPEC_H);
pb.clear([0, 0, 0, 0]);
render_retry_counter(&mut pb, &font, 2, 4, 1, 5, 1.0);
let any_painted = pb.data.iter().any(|&b| b != 0);
assert!(any_painted, "attempt>=1 must produce visible pixels");
}
#[test]
fn retry_counter_dimmer_at_half_opacity() {
let font = match decode_font_or_skip() {
Some(f) => f,
None => return,
};
let mut full = PixelBuffer::new(SPEC_W, SPEC_H);
full.clear([0, 0, 0, 0]);
let mut half = PixelBuffer::new(SPEC_W, SPEC_H);
half.clear([0, 0, 0, 0]);
render_retry_counter(&mut full, &font, 2, 4, 1, 5, 1.0);
render_retry_counter(&mut half, &font, 2, 4, 1, 5, 0.5);
let full_brightness: u64 = full.data.iter().map(|&b| b as u64).sum();
let half_brightness: u64 = half.data.iter().map(|&b| b as u64).sum();
assert!(
half_brightness < full_brightness,
"half-opacity counter should be dimmer; got full={} half={}",
full_brightness,
half_brightness
);
assert!(
half_brightness > 0,
"half-opacity counter should still be visible"
);
}
fn decode_font_or_skip() -> Option<fontdue::Font> {
super::super::render_util::load_system_font(RETRY_COUNTER_FONT_SIZE)
}
}