hydra

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

model.rs (2713B)


      1 //! The routing domain model. This is the authoritative in-memory state the daemon owns;
      2 //! [`RoutingState::snapshot`] projects it into the wire types in [`hydra_ipc`].
      3 //!
      4 //! P0 keeps this intentionally thin — sources, channel maps, monitors, and the live
      5 //! `MixSnapshot` arrive with the CoreAudio engine in P1–P3.
      6 
      7 use hydra_ipc::{DeviceSummary, RouteSummary, StateSnapshot};
      8 
      9 /// A virtual audio device Hydra publishes via the forked HAL driver.
     10 #[derive(Debug, Clone)]
     11 pub struct VirtualDevice {
     12     pub uid: String,
     13     pub name: String,
     14     pub channels: u32,
     15 }
     16 
     17 /// A routing definition: sources mixed onto a target virtual device's channels.
     18 #[derive(Debug, Clone)]
     19 pub struct Route {
     20     pub id: String,
     21     pub target_device_uid: String,
     22     /// Whether the route's CoreAudio objects are currently live.
     23     pub active: bool,
     24 }
     25 
     26 /// The daemon's complete routing state.
     27 #[derive(Debug, Default)]
     28 pub struct RoutingState {
     29     pub devices: Vec<VirtualDevice>,
     30     pub routes: Vec<Route>,
     31 }
     32 
     33 impl RoutingState {
     34     /// Project into a wire-safe snapshot for clients.
     35     pub fn snapshot(&self) -> StateSnapshot {
     36         StateSnapshot {
     37             daemon_version: crate::VERSION.to_string(),
     38             devices: self
     39                 .devices
     40                 .iter()
     41                 .map(|d| DeviceSummary { uid: d.uid.clone(), name: d.name.clone(), channels: d.channels })
     42                 .collect(),
     43             routes: self
     44                 .routes
     45                 .iter()
     46                 .map(|r| RouteSummary {
     47                     id: r.id.clone(),
     48                     target: r.target_device_uid.clone(),
     49                     source_count: 0,
     50                     active: r.active,
     51                     gain: 1.0,
     52                     muted: false,
     53                     peak: 0.0,
     54                     recording: false,
     55                 })
     56                 .collect(),
     57         }
     58     }
     59 }
     60 
     61 #[cfg(test)]
     62 mod tests {
     63     use super::*;
     64 
     65     #[test]
     66     fn empty_snapshot_carries_version() {
     67         let snap = RoutingState::default().snapshot();
     68         assert_eq!(snap.daemon_version, crate::VERSION);
     69         assert!(snap.devices.is_empty());
     70         assert!(snap.routes.is_empty());
     71     }
     72 
     73     #[test]
     74     fn snapshot_projects_devices_and_routes() {
     75         let state = RoutingState {
     76             devices: vec![VirtualDevice { uid: "hydra:main".into(), name: "Main".into(), channels: 16 }],
     77             routes: vec![Route { id: "r1".into(), target_device_uid: "hydra:main".into(), active: true }],
     78         };
     79         let snap = state.snapshot();
     80         assert_eq!(snap.devices[0].channels, 16);
     81         assert_eq!(snap.routes[0].target, "hydra:main");
     82         assert!(snap.routes[0].active);
     83     }
     84 }