use itertools::Itertools;
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use serde_json::json;
use crate::atomic_f32::AtomicF32;
use crate::config::{self, property_key::PropertyResolver};
use crate::device_kind::DeviceKind;
use crate::wirehose::{media_class, state, CommandSender, ObjectId};
pub struct View<'a> {
wirehose: &'a dyn CommandSender,
pub nodes: HashMap<ObjectId, Node>,
pub devices: HashMap<ObjectId, Device>,
pub nodes_all: Vec<ObjectId>,
pub nodes_playback: Vec<ObjectId>,
pub nodes_recording: Vec<ObjectId>,
pub nodes_output: Vec<ObjectId>,
pub nodes_input: Vec<ObjectId>,
pub devices_all: Vec<ObjectId>,
pub sinks: Vec<(Target, String)>,
pub sources: Vec<(Target, String)>,
pub default_sink: Option<Target>,
pub default_source: Option<Target>,
pub metadata_id: Option<ObjectId>,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Target {
Node(ObjectId),
Route(ObjectId, i32, i32),
Profile(ObjectId, i32),
Default,
}
impl Target {
pub fn resolve(
&self,
view: &View,
node_kind: NodeKind,
) -> Option<ObjectId> {
match self {
Target::Default => match node_kind {
NodeKind::Playback => view.default_sink,
NodeKind::Recording => view.default_source,
_ => None,
}
.and_then(|target| target.object_id()),
other => other.object_id(),
}
}
pub fn object_id(&self) -> Option<ObjectId> {
match self {
Target::Node(object_id, ..) => Some(*object_id),
Target::Route(object_id, ..) => Some(*object_id),
Target::Profile(object_id, ..) => Some(*object_id),
Target::Default => None,
}
}
}
#[derive(Debug)]
pub struct Node {
pub object_id: ObjectId,
pub object_serial: u64,
pub name: String,
pub title: String,
pub media_class: String,
pub routes: Option<Vec<(Target, String)>>,
pub target_title: String,
pub target: Option<Target>,
pub volumes: Vec<f32>,
pub mute: bool,
pub peaks: Option<Arc<[AtomicF32]>>,
pub peaks_dirty: Arc<AtomicBool>,
pub positions: Option<Vec<u32>>,
pub device_info: Option<(ObjectId, i32, i32)>,
pub is_default_sink: bool,
pub is_default_source: bool,
pub client_id: Option<ObjectId>,
}
#[derive(Debug)]
pub struct Device {
pub object_id: ObjectId,
pub object_serial: u64,
pub title: String,
pub profiles: Vec<(Target, String)>,
pub target_title: String,
pub target: Option<Target>,
}
#[derive(Debug, Clone, Copy)]
pub enum VolumeAdjustment {
Relative(f32),
Absolute(f32),
}
#[derive(Default, Debug, Clone, Copy)]
pub enum NodeKind {
Playback,
Recording,
Output,
Input,
#[default]
All,
}
#[derive(Default, Debug, Clone, Copy)]
pub enum ListKind {
Node(NodeKind),
#[default]
Device,
}
impl ListKind {
pub fn is_node(&self) -> bool {
matches!(self, ListKind::Node(_))
}
pub fn is_device(&self) -> bool {
matches!(self, ListKind::Device)
}
}
fn route_targets(
device: &state::Device,
media_class: &String,
) -> Option<Vec<(Target, String)>> {
let profile_index = device.profile_index?;
let profile = device.profiles.get(&profile_index)?;
let profile_devices = profile
.classes
.iter()
.find_map(|(mc, devices)| (mc == media_class).then_some(devices))?;
Some(
device
.enum_routes
.values()
.filter_map(|route| {
if !route.profiles.contains(&profile_index) {
return None;
}
let route_device =
route.devices.iter().find(|route_device| {
profile_devices.contains(route_device)
})?;
let title = if route.available {
route.description.clone()
} else {
format!("{} (unavailable)", route.description)
};
Some((
Target::Route(device.object_id, route.index, *route_device),
title,
))
})
.collect(),
)
}
fn active_route(
device: &state::Device,
card_device: i32,
) -> Option<&state::Route> {
let profile_index = device.profile_index?;
device
.routes
.get(&card_device)
.filter(|route| route.profiles.contains(&profile_index))
}
impl Node {
fn from(
state: &state::State,
names: &config::Names,
sources: &[(Target, String)],
sinks: &[(Target, String)],
default_sink_name: &Option<String>,
default_source_name: &Option<String>,
node: &state::Node,
) -> Option<Node> {
let object_id = node.object_id;
let media_class = node.props.media_class()?.clone();
let title = names.resolve(state, node)?;
let (volumes, mute, device_info) =
if let Some(device_id) = node.props.device_id() {
let device = state.devices.get(device_id)?;
let card_device = *node.props.card_profile_device()?;
if let Some(route) = active_route(device, card_device) {
let route_index = route.index;
(
route.volumes.clone(),
route.mute,
Some((*device_id, route_index, card_device)),
)
} else {
(node.volumes.as_ref()?.clone(), node.mute?, None)
}
} else {
(node.volumes.as_ref()?.clone(), node.mute?, None)
};
let (routes, target, target_title) = if let Some(device_id) =
node.props.device_id()
{
let device = state.devices.get(device_id)?;
let card_device = *node.props.card_profile_device()?;
let mut routes: Vec<_> =
route_targets(device, &media_class).unwrap_or_default();
routes.sort_by(|(_, a), (_, b)| a.cmp(b));
let routes = routes;
let (target, target_title) = match active_route(device, card_device)
{
Some(route) => {
let target_title = if route.available {
route.description.clone()
} else {
format!("{} (unavailable)", route.description)
};
(
Some(Target::Route(
device.object_id,
route.index,
card_device,
)),
target_title,
)
}
None => (None, String::from("No route selected")),
};
(Some(routes), target, target_title)
} else if media_class::is_sink_input(&media_class) {
let outputs = state.outputs(object_id);
let sink = sinks.iter().find(|(target, _)| {
matches!(target, Target::Node(sink_id)
if outputs.contains(sink_id))
});
let (target, target_title) = if !has_target(state, node.object_id) {
(
Some(Target::Default),
sink.map(|(_, title)| title.clone())
.unwrap_or(String::from("No default")),
)
} else {
(
sink.map(|&(target, _)| target),
sink.map(|(_, title)| title.clone()).unwrap_or_default(),
)
};
(None, target, target_title)
} else if media_class::is_source_output(&media_class) {
let inputs = state.inputs(object_id);
let source = sources.iter().find(|(target, _)| {
matches!(target, Target::Node(source_id)
if inputs.contains(source_id))
});
let (target, target_title) = if !has_target(state, node.object_id) {
(
Some(Target::Default),
source
.map(|(_, title)| title.clone())
.unwrap_or(String::from("No default")),
)
} else {
(
source.map(|&(target, _)| target),
source.map(|(_, title)| title.clone()).unwrap_or_default(),
)
};
(None, target, target_title)
} else {
(None, None, String::from("No route selected"))
};
Some(Self {
object_id,
object_serial: *node.props.object_serial()?,
name: node.props.node_name()?.clone(),
title,
media_class,
routes,
target,
target_title,
volumes,
mute,
peaks: node.peaks.as_ref().map(Arc::clone),
peaks_dirty: Arc::clone(&node.peaks_dirty),
positions: node.positions.clone(),
device_info,
is_default_sink: default_sink_name.as_ref()
== node.props.node_name(),
is_default_source: default_source_name.as_ref()
== node.props.node_name(),
client_id: node.props.client_id().copied(),
})
}
}
impl Device {
fn from(
state: &state::State,
device: &state::Device,
names: &config::Names,
) -> Option<Device> {
let object_id = device.object_id;
let title = names.resolve(state, device)?;
let mut profiles: Vec<_> = device
.profiles
.values()
.map(|profile| {
let title = if profile.available {
profile.description.clone()
} else {
format!("{} (unavailable)", profile.description)
};
(profile.index, title)
})
.collect();
profiles.sort_by_key(|&(index, _)| index);
let profiles = profiles
.into_iter()
.map(|(index, title)| (Target::Profile(object_id, index), title))
.collect();
let target_profile = device.profiles.get(&device.profile_index?)?;
let target_title = if target_profile.available {
target_profile.description.clone()
} else {
format!("{} (unavailable)", target_profile.description)
};
let target = Some(Target::Profile(object_id, device.profile_index?));
let object_serial = *device.props.object_serial()?;
Some(Device {
object_id,
object_serial,
title,
profiles,
target_title,
target,
})
}
}
fn default_for(state: &state::State, which: &str) -> Option<String> {
let metadata = state.get_metadata_by_name("default")?;
let json = metadata.properties.get(&0)?.get(which)?;
let object = serde_json::from_str::<serde_json::Value>(json).ok()?;
Some(String::from(object["name"].as_str()?))
}
fn target_node(state: &state::State, node_id: ObjectId) -> Option<i64> {
let metadata = state.get_metadata_by_name("default")?;
let json = metadata
.properties
.get(&node_id.into())?
.get("target.node")?;
serde_json::from_str(json).ok()
}
fn target_object(state: &state::State, node_id: ObjectId) -> Option<i64> {
let metadata = state.get_metadata_by_name("default")?;
let json = metadata
.properties
.get(&node_id.into())?
.get("target.object")?;
serde_json::from_str(json).ok()
}
fn has_target(state: &state::State, node_id: ObjectId) -> bool {
match (target_node(state, node_id), target_object(state, node_id)) {
(Some(node), _) if node != -1 => true,
(_, Some(object)) if object != -1 => true,
_ => false,
}
}
fn is_filtered(
filters: &[config::MatchCondition],
state: &state::State,
object: Option<&(impl PropertyResolver + ?Sized)>,
) -> bool {
object.is_some_and(|object| {
filters
.iter()
.any(|condition| condition.matches(state, object))
})
}
impl<'a> View<'a> {
pub fn new(wirehose: &'a dyn CommandSender) -> View<'a> {
Self {
wirehose,
nodes: Default::default(),
devices: Default::default(),
nodes_all: Default::default(),
nodes_playback: Default::default(),
nodes_recording: Default::default(),
nodes_output: Default::default(),
nodes_input: Default::default(),
devices_all: Default::default(),
sinks: Default::default(),
sources: Default::default(),
default_sink: Default::default(),
default_source: Default::default(),
metadata_id: Default::default(),
}
}
pub fn from(
wirehose: &'a dyn CommandSender,
state: &state::State,
names: &config::Names,
filters: &[config::MatchCondition],
) -> View<'a> {
let default_sink_name = default_for(state, "default.audio.sink");
let default_source_name = default_for(state, "default.audio.source");
let default_sink =
default_sink_name.as_ref().and_then(|default_sink_name| {
state
.nodes
.values()
.find(|node| {
node.props.node_name() == Some(default_sink_name)
})
.map(|node| Target::Node(node.object_id))
});
let default_source =
default_source_name
.as_ref()
.and_then(|default_source_name| {
state
.nodes
.values()
.find(|node| {
node.props.node_name() == Some(default_source_name)
})
.map(|node| Target::Node(node.object_id))
});
let mut sinks: Vec<_> = state
.nodes
.values()
.filter_map(|node| {
if media_class::is_sink(node.props.media_class()?) {
Some((
Target::Node(node.object_id),
names.resolve(state, node)?,
))
} else {
None
}
})
.collect();
sinks.sort_by(|(_, a), (_, b)| a.cmp(b));
let sinks = sinks;
let mut sources: Vec<_> = state
.nodes
.values()
.filter_map(|node| {
if media_class::is_source(node.props.media_class()?) {
let title = names.resolve(state, node)?;
Some((Target::Node(node.object_id), title))
} else if media_class::is_sink(node.props.media_class()?) {
let title = names.resolve(state, node)?;
Some((
Target::Node(node.object_id),
format!("Monitor of {title}"),
))
} else {
None
}
})
.collect();
sources.sort_by(|(_, a), (_, b)| a.cmp(b));
let sources = sources;
let nodes: HashMap<ObjectId, Node> = state
.nodes
.values()
.filter_map(|node| {
Node::from(
state,
names,
&sources,
&sinks,
&default_sink_name,
&default_source_name,
node,
)
})
.map(|node| (node.object_id, node))
.collect();
let devices: HashMap<ObjectId, Device> = state
.devices
.values()
.filter(|device| {
!is_filtered(
filters,
state,
state.devices.get(&device.object_id),
)
})
.filter_map(|device| Device::from(state, device, names))
.map(|device| (device.object_id, device))
.collect();
let mut nodes_all = Vec::new();
let mut nodes_playback = Vec::new();
let mut nodes_recording = Vec::new();
let mut nodes_output = Vec::new();
let mut nodes_input = Vec::new();
for (id, node) in nodes
.iter()
.sorted_by_key(|(_, node)| node.object_serial)
.filter(|(_, node)| {
!is_filtered(filters, state, state.nodes.get(&node.object_id))
})
{
nodes_all.push(*id);
if media_class::is_sink_input(&node.media_class) {
nodes_playback.push(*id);
}
if media_class::is_source_output(&node.media_class) {
nodes_recording.push(*id);
}
if media_class::is_sink(&node.media_class) {
nodes_output.push(*id);
}
if media_class::is_source(&node.media_class) {
nodes_input.push(*id);
}
}
let nodes_all = nodes_all;
let nodes_playback = nodes_playback;
let nodes_recording = nodes_recording;
let nodes_output = nodes_output;
let nodes_input = nodes_input;
let devices_all = devices
.iter()
.sorted_by_key(|(_, device)| device.object_serial)
.map(|(&id, _)| id)
.collect();
Self {
wirehose,
nodes,
devices,
nodes_all,
nodes_playback,
nodes_recording,
nodes_output,
nodes_input,
devices_all,
sinks,
sources,
default_sink,
default_source,
metadata_id: state.metadatas_by_name.get("default").copied(),
}
}
pub fn set_default(&self, node_id: ObjectId, device_kind: DeviceKind) {
let Some(node) = self.nodes.get(&node_id) else {
return;
};
let Some(metadata_id) = self.metadata_id else {
return;
};
let key = match device_kind {
DeviceKind::Source => "default.configured.audio.source",
DeviceKind::Sink => "default.configured.audio.sink",
};
self.wirehose.metadata_set_property(
metadata_id,
0,
String::from(key),
Some(String::from("Spa:String:JSON")),
Some(json!({ "name": &node.name }).to_string()),
);
}
pub fn set_target(&self, node_id: ObjectId, target: Target) {
let Some(metadata_id) = self.metadata_id else {
return;
};
match target {
Target::Default => {
self.wirehose.metadata_set_property(
metadata_id,
node_id.into(),
String::from("target.object"),
Some(String::from("Spa:Id")),
Some(String::from("-1")),
);
self.wirehose.metadata_set_property(
metadata_id,
node_id.into(),
String::from("target.node"),
Some(String::from("Spa:Id")),
Some(String::from("-1")),
);
}
Target::Node(target_id) => {
self.wirehose.metadata_set_property(
metadata_id,
node_id.into(),
String::from("target.object"),
None,
None,
);
self.wirehose.metadata_set_property(
metadata_id,
node_id.into(),
String::from("target.node"),
Some(String::from("Spa:Id")),
Some(target_id.to_string()),
);
}
Target::Route(device_id, route_index, route_device) => {
self.wirehose.device_set_route(
device_id,
route_index,
route_device,
);
}
Target::Profile(device_id, profile_index) => {
self.wirehose.device_set_profile(device_id, profile_index);
}
}
}
pub fn mute(&self, node_id: ObjectId) {
let Some(node) = self.nodes.get(&node_id) else {
return;
};
let mute = !node.mute;
if let Some((device_id, route_index, route_device)) = node.device_info {
self.wirehose.device_mute(
device_id,
route_index,
route_device,
mute,
);
} else {
self.wirehose.node_mute(node_id, mute);
}
}
pub fn volume(
&self,
node_id: ObjectId,
adjustment: VolumeAdjustment,
max: Option<f32>,
) -> bool {
let Some(node) = self.nodes.get(&node_id) else {
return false;
};
let mut volumes = node.volumes.clone();
if volumes.is_empty() {
return false;
}
match adjustment {
VolumeAdjustment::Relative(delta) => {
let avg = volumes.iter().sum::<f32>() / volumes.len() as f32;
volumes.fill((avg.cbrt() + delta).max(0.0).powi(3));
}
VolumeAdjustment::Absolute(volume) => {
volumes.fill(volume.max(0.0).powi(3));
}
}
let volumes = volumes;
if let Some(max) = max {
if volumes
.iter()
.any(|volume| (volume.cbrt() * 100.0).round() > max)
{
return false;
}
}
if let Some((device_id, route_index, route_device)) = node.device_info {
self.wirehose.device_volumes(
device_id,
route_index,
route_device,
volumes,
);
} else {
self.wirehose.node_volumes(node_id, volumes);
}
true
}
pub fn object_ids(&self, node_kind: ListKind) -> &[ObjectId] {
match node_kind {
ListKind::Node(NodeKind::Playback) => &self.nodes_playback,
ListKind::Node(NodeKind::Recording) => &self.nodes_recording,
ListKind::Node(NodeKind::Output) => &self.nodes_output,
ListKind::Node(NodeKind::Input) => &self.nodes_input,
ListKind::Node(NodeKind::All) => &self.nodes_all,
ListKind::Device => &self.devices_all,
}
}
pub fn full_nodes(&self, node_kind: NodeKind) -> Vec<&Node> {
let node_ids = self.object_ids(ListKind::Node(node_kind));
node_ids
.iter()
.filter_map(|node_id| self.nodes.get(node_id))
.collect()
}
pub fn full_devices(&self) -> Vec<&Device> {
let device_ids = self.object_ids(ListKind::Device);
device_ids
.iter()
.filter_map(|device_id| self.devices.get(device_id))
.collect()
}
pub fn next_id(
&self,
list_kind: ListKind,
object_id: Option<ObjectId>,
) -> Option<ObjectId> {
let objects = self.object_ids(list_kind);
let next_index = match object_id {
Some(object_id) => objects
.iter()
.position(|&id| id == object_id)?
.saturating_add(1),
None => 0,
};
objects.get(next_index).copied()
}
pub fn previous_id(
&self,
list_kind: ListKind,
object_id: Option<ObjectId>,
) -> Option<ObjectId> {
let objects = self.object_ids(list_kind);
let next_index = match object_id {
Some(object_id) => objects
.iter()
.position(|&id| id == object_id)?
.saturating_sub(1),
None => 0,
};
objects.get(next_index).copied()
}
pub fn position(
&self,
list_kind: ListKind,
object_id: ObjectId,
) -> Option<usize> {
self.object_ids(list_kind)
.iter()
.position(|&id| id == object_id)
}
pub fn len(&self, list_kind: ListKind) -> usize {
self.object_ids(list_kind).len()
}
pub fn node_targets(
&self,
node_id: ObjectId,
) -> Option<(Vec<(Target, String)>, usize)> {
let node = self.nodes.get(&node_id)?;
let (mut targets, default) = if let Some(routes) = &node.routes {
(routes.clone(), None)
} else if media_class::is_sink_input(&node.media_class) {
(self.sinks.clone(), self.default_sink)
} else if media_class::is_source_output(&node.media_class) {
(self.sources.clone(), self.default_source)
} else {
(Vec::new(), None)
};
let default_name = default
.and_then(|default| {
targets
.iter()
.find(|(target, _)| *target == default)
.map(|(_, name)| format!("Default: {name}"))
})
.unwrap_or(String::from("Default: No default"));
targets.sort_by(|(_, a), (_, b)| a.cmp(b));
if media_class::is_sink_input(&node.media_class)
|| media_class::is_source_output(&node.media_class)
{
targets.insert(0, (Target::Default, default_name.clone()));
};
let targets = targets;
let selected_position = node
.target
.and_then(|node_target| {
targets
.iter()
.position(|&(target, _)| target == node_target)
})
.unwrap_or(0);
Some((targets, selected_position))
}
pub fn device_targets(
&self,
device_id: ObjectId,
) -> Option<(Vec<(Target, String)>, usize)> {
let device = self.devices.get(&device_id)?;
let targets = device.profiles.clone();
let selected_position = device
.target
.and_then(|device_target| {
targets
.iter()
.position(|&(target, _)| target == device_target)
})
.unwrap_or(0);
Some((targets, selected_position))
}
}