hydra

Terminal replacement for Loopback — virtual audio devices and routing on macOS, from a ratatui TUI.
Log | Files | Refs | README | LICENSE

commit 05eef3c033b90cf482b966a39dfc89d624796a38
parent bfea67a8601b792059b95d180ffc2a1c41663346
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date:   Sun, 31 May 2026 23:03:04 -0500

fix: usable capture level — makeup-gain default + multiplicative gain steps

Routing to Hydra worked after the tap-buffer fix but was unusably quiet:
process taps attenuate ~-20dB and +/- stepped additively by 0.05, so reaching
~10x makeup gain took ~180 presses (the "too quiet even maxed" report).

- New routes default to gain 10.0 (DEFAULT_GAIN): a 0.6 source now lands at
  peak 0.60 out of the Hydra device immediately — verified.
- +/- are now multiplicative (×1.4 ≈ +3dB/press), spanning 0..16 in a few keys.

Verified through the rebuilt installed bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Diffstat:
Mcrates/hydra/src/app.rs | 25++++++++++++++++++-------
Mcrates/hydra/src/main.rs | 4++--
2 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/crates/hydra/src/app.rs b/crates/hydra/src/app.rs @@ -4,6 +4,11 @@ use hydra_ipc::{AudioApp, AudioDevice, Command, Response, RouteSummary}; use crate::client; +/// Default gain for a new route. Core Audio process taps attenuate the captured signal +/// (~-20 dB observed), so a fresh route at unity is far too quiet to be usable; ~10x makeup +/// gain restores roughly the source level. Tunable per-route with +/- in the TUI. +const DEFAULT_GAIN: f32 = 10.0; + /// Whether we're currently talking to the daemon. #[derive(Debug, Clone)] pub enum Connection { @@ -166,7 +171,7 @@ impl App { let (pid, name) = (app.pid, app.name.clone()); let output_uid = self.selected_output().map(|d| d.uid.clone()); let dest = self.selected_output().map(|d| d.name.as_str()).unwrap_or("default output").to_string(); - match client::request(Command::StartMonitor { pid, output_uid, gain: 1.0 }) { + match client::request(Command::StartMonitor { pid, output_uid, gain: DEFAULT_GAIN }) { Ok(Response::RouteStarted { id }) => self.status = format!("{id}: {name} → {dest}"), Ok(Response::Error(e)) => self.status = format!("start failed: {e}"), Ok(other) => self.status = format!("unexpected: {other:?}"), @@ -199,7 +204,7 @@ impl App { } let output_uid = self.selected_output().map(|d| d.uid.clone()); let dest = self.selected_output().map(|d| d.name.as_str()).unwrap_or("default output").to_string(); - match client::request(Command::StartCombined { pids: pids.clone(), output_uid, gain: 1.0 }) { + match client::request(Command::StartCombined { pids: pids.clone(), output_uid, gain: DEFAULT_GAIN }) { Ok(Response::RouteStarted { id }) => { self.status = format!("{id}: {} sources → {dest}", pids.len()); self.marked.clear(); @@ -224,12 +229,18 @@ impl App { self.refresh(); } - /// Nudge the selected route's gain by `delta`, clamped to 0.0..=2.0. - pub fn adjust_gain(&mut self, delta: f32) { + /// Scale the selected route's gain by `factor` (multiplicative, so a few presses span + /// the whole range). `up=true` multiplies, `up=false` divides. Clamped to 0..=16. + /// Multiplicative because Core Audio process taps attenuate ~-20 dB, so useful makeup + /// gain is ~10x — unreachable with additive 0.05 steps. + pub fn adjust_gain(&mut self, up: bool) { let Some(route) = self.routes.get(self.route_sel) else { return }; - // Ceiling is generous: Core Audio process taps attenuate the captured signal - // (~-20 dB observed), so makeup gain well above unity is normal here. - let gain = (route.gain + delta).clamp(0.0, 16.0); + const STEP: f32 = 1.4; // ~+3 dB per press + let gain = if up { + (route.gain.max(0.05) * STEP).clamp(0.0, 16.0) + } else { + (route.gain / STEP).clamp(0.0, 16.0) + }; let _ = client::request(Command::SetGain { id: route.id.clone(), gain }); self.refresh(); } diff --git a/crates/hydra/src/main.rs b/crates/hydra/src/main.rs @@ -78,8 +78,8 @@ fn handle_key(app: &mut App, code: KeyCode) { KeyCode::Char('a') => app.toggle_show_all(), KeyCode::Char('m') => app.toggle_mute_selected(), KeyCode::Char('d') | KeyCode::Char('x') => app.stop_selected(), - KeyCode::Char('+') | KeyCode::Char('=') => app.adjust_gain(0.05), - KeyCode::Char('-') | KeyCode::Char('_') => app.adjust_gain(-0.05), + KeyCode::Char('+') | KeyCode::Char('=') => app.adjust_gain(true), + KeyCode::Char('-') | KeyCode::Char('_') => app.adjust_gain(false), _ => {} } }