cosmic-settings/crates/cosmic-pipewire/src/lib.rs
2025-12-24 05:25:43 +01:00

1059 lines
36 KiB
Rust

// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
// #![deny(missing_docs)]
pub mod device;
pub use device::Device;
pub mod node;
use intmap::IntMap;
pub use node::{MediaClass, Node, NodeProps};
mod profile;
pub use profile::Profile;
mod route;
#[cfg(feature = "route-port-type")]
pub use route::PortType;
pub use route::{Route, RouteProps};
mod spa_utils;
pub use spa_utils::Channel;
use libspa::{
param::{ParamType, format::FormatProperties},
pod::{self, Pod, serialize::PodSerializer},
utils::SpaTypes,
};
use pipewire::{
device::{DeviceChangeMask, DeviceListener},
main_loop::MainLoopWeak,
metadata::MetadataListener,
node::NodeListener,
proxy::{ProxyListener, ProxyT},
types::ObjectType,
};
use std::{cell::RefCell, rc::Rc, u32};
pub type NodeId = u32;
pub type RouteId = u32;
pub type DeviceId = u32;
pub type ProfileId = i32;
pub type PipewireId = u32;
pub fn run(on_event: impl FnMut(Event) + Send + 'static) -> Sender {
let (request_tx, request_rx) = pipewire::channel::channel();
std::thread::spawn(move || {
let on_event = Box::new(on_event);
if let Err(why) = run_service(request_rx, on_event) {
tracing::error!(?why, "failed to run pipewire thread");
}
});
Sender(request_tx)
}
/// Monitor pipewire activity and
fn run_service(
rx: pipewire::channel::Receiver<Request>,
on_event: Box<dyn FnMut(Event)>,
) -> Result<(), pipewire::Error> {
let main_loop = pipewire::main_loop::MainLoopRc::new(None)?;
let context = pipewire::context::ContextRc::new(&main_loop, None)?;
let core = context.connect_rc(None)?;
let registry = core.get_registry_rc()?;
let state = Rc::new(RefCell::new(State {
nodes: IntMap::new(),
proxies: Proxies {
devices: IntMap::new(),
metadata: IntMap::new(),
nodes: IntMap::new(),
},
routes: IntMap::new(),
node_devices: IntMap::new(),
node_card_profile_device: IntMap::new(),
node_props: IntMap::new(),
main_loop: main_loop.downgrade(),
on_event,
}));
let _request_handler = rx.attach(main_loop.loop_(), {
let state = Rc::downgrade(&state);
move |request| match request {
Request::EnumerateDevice(id) => {
if let Some(state) = state.upgrade() {
state.borrow_mut().enumerate_device(id);
}
}
Request::SetRoute(id, card_profile_device, route) => {
if let Some(state) = state.upgrade() {
state
.borrow_mut()
.set_route(id, card_profile_device as i32, route as i32);
}
}
Request::SetNodeVolume(id, volume, balance) => {
if let Some(state) = state.upgrade() {
state.borrow_mut().set_node_volume(id, volume, balance);
}
}
Request::SetNodeMute(id, mute) => {
if let Some(state) = state.upgrade() {
state.borrow_mut().set_mute_node(id, mute);
}
}
Request::SetProfile(id, index, save) => {
if let Some(state) = state.upgrade() {
state.borrow_mut().set_profile(id, index, save);
}
}
Request::Quit => {
if let Some(state) = state.upgrade() {
state.borrow_mut().quit();
}
}
}
});
let registry_weak = registry.downgrade();
let _registry_listener = registry
.add_listener_local()
.global(move |obj| {
let Some(registry) = registry_weak.upgrade() else {
return;
};
match obj.type_ {
ObjectType::Device => {
let Ok(device) = registry.bind::<pipewire::device::Device, _>(obj) else {
return;
};
device.subscribe_params(&[
ParamType::EnumProfile,
ParamType::EnumRoute,
ParamType::Profile,
ParamType::Route,
]);
let pw_id = device.upcast_ref().id();
let listener = device
.add_listener_local()
.info({
let state = Rc::downgrade(&state);
move |info| {
let change_mask = info.change_mask();
if change_mask == DeviceChangeMask::PARAMS {
if let Some(state) = state.upgrade() {
let state = state.borrow();
let Some((_device_id, device, ..)) =
state.proxies.devices.get(pw_id)
else {
return;
};
device.enum_params(
0,
Some(ParamType::EnumProfile),
0,
u32::MAX,
);
device.enum_params(
0,
Some(ParamType::Profile),
0,
u32::MAX,
);
device.enum_params(1, Some(ParamType::Route), 0, u32::MAX);
}
return;
}
if let Some(device) = Device::from_device(info) {
if let Some(state) = state.upgrade() {
state.borrow_mut().add_device(pw_id, device);
}
}
}
})
.param({
let state = Rc::downgrade(&state);
move |_seq, param_type, index, _next, param| {
let Some(pod) = param else {
return;
};
let Some(state) = state.upgrade() else {
return;
};
let Some(&(device_id, ..)) =
state.borrow().proxies.devices.get(pw_id)
else {
return;
};
match param_type {
ParamType::EnumProfile => {
if let Some(profile) = Profile::from_pod(pod) {
state.borrow_mut().add_profile(device_id, profile);
}
}
ParamType::EnumRoute => {
if let Some(route) = Route::from_pod(pod) {
state.borrow_mut().add_route(device_id, index, route);
}
}
ParamType::Profile => {
if let Some(profile) = Profile::from_pod(pod) {
state.borrow_mut().active_profile(device_id, profile);
}
}
ParamType::Route => {
if let Some(route) = Route::from_pod(pod) {
state
.borrow_mut()
.active_route(device_id, index, route);
}
}
_ => (),
}
}
})
.register();
let proxy = device.upcast_ref();
let remove_listener = proxy
.add_listener_local()
.removed({
let state = Rc::downgrade(&state);
move || {
if let Some(state) = state.upgrade() {
let Some((id, ..)) =
state.borrow_mut().proxies.devices.remove(pw_id)
else {
return;
};
state.borrow_mut().remove_device(id);
}
}
})
.register();
state
.borrow_mut()
.proxies
.devices
.insert(pw_id, (0, device, listener, remove_listener));
}
ObjectType::Node => {
let Ok(node) = registry.bind::<pipewire::node::Node, _>(obj) else {
return;
};
node.subscribe_params(&[ParamType::Props]);
let id = node.upcast_ref().id();
let listener = node
.add_listener_local()
.info({
let state = Rc::downgrade(&state);
move |info| {
if let Some(node) = Node::from_node(info) {
if let Some(state) = state.upgrade() {
state.borrow_mut().add_node(id, node);
}
}
}
})
.param({
let state = Rc::downgrade(&state);
move |_seq, param_type, _index, _next, param| {
let Some(pod) = param else {
return;
};
let Some(state) = state.upgrade() else {
return;
};
let Some(&(node_id, ..)) = state.borrow().proxies.nodes.get(id)
else {
return;
};
match param_type {
ParamType::Props => {
if let Some(props) = NodeProps::from_pod(pod) {
state.borrow_mut().set_node_props(node_id, props);
}
}
_ => (),
}
}
})
.register();
let remove_listener = node
.upcast_ref()
.add_listener_local()
.removed({
let state = Rc::downgrade(&state);
move || {
if let Some(state) = state.upgrade() {
state.borrow_mut().remove_node(id);
}
}
})
.register();
state
.borrow_mut()
.proxies
.nodes
.insert(id, (0, node, listener, remove_listener));
}
ObjectType::Metadata => {
let Ok(metadata) = registry.bind::<pipewire::metadata::Metadata, _>(obj) else {
return;
};
let id = metadata.upcast_ref().id();
let listener = metadata
.add_listener_local()
.property({
let state = Rc::downgrade(&state);
move |_subject, key, _type, value| {
let Some((key, value)) = key.zip(value) else {
return 0;
};
match key {
"default.audio.sink" => {
if let Ok(value) =
serde_json::de::from_str::<DefaultAudio>(value)
{
if let Some(state) = state.upgrade() {
state
.borrow_mut()
.default_sink(value.name.to_owned())
}
}
}
"default.audio.source" => {
if let Ok(value) =
serde_json::de::from_str::<DefaultAudio>(value)
{
if let Some(state) = state.upgrade() {
state
.borrow_mut()
.default_source(value.name.to_owned())
}
}
}
_ => (),
}
0
}
})
.register();
let remove_listener = metadata
.upcast_ref()
.add_listener_local()
.removed({
let state = Rc::downgrade(&state);
move || {
if let Some(state) = state.upgrade() {
state.borrow_mut().remove_metadata(id);
}
}
})
.register();
state
.borrow_mut()
.proxies
.metadata
.insert(id, (metadata, listener, remove_listener));
}
_ => {}
};
})
.register();
main_loop.run();
Ok(())
}
/// Response from pipewire
#[derive(Clone, Debug)]
pub enum Event {
/// Set the active profile for a device
ActiveProfile(DeviceId, Profile),
/// Set the active route for a device
ActiveRoute(DeviceId, u32, Route),
/// A new device was detected.
AddDevice(Device),
/// A new node was detected.
AddNode(Node),
/// A profile was enumerated
AddProfile(DeviceId, Profile),
/// A route was enumerated
AddRoute(DeviceId, u32, Route),
/// The default sink was changed.
DefaultSink(String),
/// The default source was changed.
DefaultSource(String),
/// Emitted when the properties of a node has changed.
NodeProperties(NodeId, NodeProps),
/// A device with the given device_id was removed.
RemoveDevice(DeviceId),
/// A node with the given object_id was removed.
RemoveNode(NodeId),
}
#[derive(Clone, Debug)]
pub enum Request {
/// Request a device's routes, profiles, active routes, and active profile.
EnumerateDevice(DeviceId),
/// Mute a node ID
SetNodeMute(NodeId, bool),
/// Set a device profile by profile index.
SetProfile(DeviceId, u32, bool),
/// Set a new volume
SetNodeVolume(DeviceId, f32, Option<f32>),
/// Change route of a device
SetRoute(DeviceId, u32, u32),
/// Stop the main loop and exit the thread.
Quit,
}
#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Availability {
#[default]
Unknown,
No,
Yes,
}
#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Direction {
Input,
#[default]
Output,
}
#[derive(serde::Deserialize)]
pub struct DefaultAudio<'a> {
name: &'a str,
}
struct Proxies {
devices: IntMap<
PipewireId,
(
DeviceId,
pipewire::device::Device,
DeviceListener,
ProxyListener,
),
>,
nodes: IntMap<PipewireId, (NodeId, pipewire::node::Node, NodeListener, ProxyListener)>,
metadata: IntMap<
PipewireId,
(
pipewire::metadata::Metadata,
MetadataListener,
ProxyListener,
),
>,
}
struct State {
nodes: IntMap<PipewireId, (NodeId, Option<DeviceId>)>,
pub(self) proxies: Proxies,
routes: IntMap<DeviceId, Vec<Route>>,
node_devices: IntMap<NodeId, DeviceId>,
node_props: IntMap<NodeId, NodeProps>,
node_card_profile_device: IntMap<NodeId, u32>,
main_loop: MainLoopWeak,
/// Handle events and exit the loop when `true` is returned.
on_event: Box<dyn FnMut(Event)>,
}
impl State {
fn active_profile(&mut self, id: DeviceId, profile: Profile) {
self.on_event(Event::ActiveProfile(id, profile));
}
fn active_route(&mut self, id: DeviceId, index: u32, route: Route) {
self.on_event(Event::ActiveRoute(id, index, route));
}
fn add_device(&mut self, id: PipewireId, device: Device) {
// Map the device's pipewire ID to its device ID
if let Some(entry) = self.proxies.devices.get_mut(id) {
entry.0 = device.id;
};
let device_id = device.id;
self.on_event(Event::AddDevice(device));
// Request the device's profiles and properties now that we've registered it.
self.enumerate_device(device_id);
}
fn add_node(&mut self, id: PipewireId, node: Node) {
// Map the device's pipewire ID to its device ID
if let Some(entry) = self.proxies.nodes.get_mut(id) {
entry.0 = node.object_id;
// Request properties for this node now that we've registered it.
entry.1.enum_params(0, Some(ParamType::Props), 0, u32::MAX);
};
// Track the node's node ID and device ID by its pipewire ID.
self.nodes.insert(id, (node.object_id, node.device_id));
// And the associated route device that the node is derived from.
if let Some(card_profile_device) = node.card_profile_device {
self.node_card_profile_device
.insert(node.object_id, card_profile_device);
}
// Track the node's device ID by its node ID.
if let Some(device_id) = node.device_id {
self.node_devices.insert(node.object_id, device_id);
}
self.on_event(Event::AddNode(node));
}
fn add_profile(&mut self, id: DeviceId, profile: Profile) {
self.on_event(Event::AddProfile(id, profile));
}
fn add_route(&mut self, id: DeviceId, index: u32, route: Route) {
// Keep a record of routes attached to a device for setting properties.
// This will overwrite routes on updates to
let routes = self.routes.entry(id).or_default();
if routes.len() < index as usize + 1 {
let additional = (index as usize + 1) - routes.capacity();
routes.reserve_exact(additional);
routes.extend(std::iter::repeat(Route::default()).take(additional));
}
routes[index as usize] = route.clone();
self.on_event(Event::AddRoute(id, index, route));
}
/// Request a device's profiles and routes.
fn enumerate_device(&mut self, id: DeviceId) {
let Some(device) = self.device(id) else {
return;
};
device.enum_params(0, Some(ParamType::EnumProfile), 0, u32::MAX);
device.enum_params(1, Some(ParamType::EnumRoute), 0, u32::MAX);
device.enum_params(2, Some(ParamType::Profile), 0, u32::MAX);
device.enum_params(3, Some(ParamType::Route), 0, u32::MAX);
}
fn default_sink(&mut self, name: String) {
self.on_event(Event::DefaultSink(name));
}
fn default_source(&mut self, name: String) {
self.on_event(Event::DefaultSource(name));
}
fn node_route(&self, device_id: DeviceId, route_device: i32) -> Option<&Route> {
self.routes
.get(device_id)?
.iter()
.find(|r| r.devices.contains(&route_device))
}
fn on_event(&mut self, event: Event) {
(self.on_event)(event);
}
fn quit(&mut self) {
if let Some(main_loop) = self.main_loop.upgrade() {
main_loop.quit();
}
}
fn remove_device(&mut self, id: PipewireId) {
if let Some((device_id, ..)) = self.proxies.devices.remove(id) {
self.routes.remove(device_id);
self.on_event(Event::RemoveDevice(device_id));
}
}
fn remove_metadata(&mut self, id: PipewireId) {
self.proxies.metadata.remove(id);
}
fn remove_node(&mut self, id: PipewireId) {
if let Some((node_id, _)) = self.nodes.remove(id) {
self.node_card_profile_device.remove(node_id);
self.node_devices.remove(node_id);
self.node_props.remove(node_id);
self.on_event(Event::RemoveNode(node_id));
}
self.proxies.nodes.remove(id);
}
fn set_mute(&self, id: DeviceId, route_device: i32, route: &Route, mute: bool) {
let Some(device) = self.device(id) else {
return;
};
let route_props = pod::object!(
SpaTypes::ObjectParamProps,
ParamType::Props,
pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, mute),
);
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamRoute,
ParamType::Route,
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
Int,
route.index
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
Int,
route_device
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_props),
Object,
route_props
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_save),
Bool,
true
)
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
device.set_param(ParamType::Route, 0, param);
}
}
fn set_mute_node(&self, id: NodeId, mute: bool) {
// Prefer to mute the device instead of the node.
// Muting a node will not emit a notification.
if let Some((&device_id, &route_device)) = self
.node_devices
.get(id)
.zip(self.node_card_profile_device.get(id))
{
let route_device = route_device as i32;
if let Some(route) = self.node_route(device_id, route_device) {
self.set_mute(device_id, route_device, route, mute);
return;
};
}
let Some(node) = self.node(id) else {
return;
};
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamProps,
ParamType::Props,
pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, mute),
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
node.set_param(ParamType::Props, 0, param);
}
}
fn set_route(&self, device_id: DeviceId, card_profile_device: i32, route_index: i32) {
let Some(device) = self.device(device_id) else {
return;
};
tracing::debug!(target: "sound", "set_route device_id {device_id}, route_index {route_index}");
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamRoute,
ParamType::Route,
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
Int,
route_index
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
Int,
card_profile_device
),
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
device.set_param(ParamType::Route, 0, param);
}
return;
}
fn set_node_props(&mut self, id: NodeId, props: NodeProps) {
self.on_event(Event::NodeProperties(id, props.clone()));
*self.node_props.entry(id).or_default() = props;
}
fn set_node_volume(&self, id: NodeId, volume: f32, balance: Option<f32>) {
let Some(props) = self.node_props.get(id) else {
return;
};
// Prefer to change the volume of the device instead of the node.
if let Some((&device_id, &route_device)) = self
.node_devices
.get(id)
.zip(self.node_card_profile_device.get(id))
{
let route_device = route_device as i32;
if let Some(route) = self.node_route(device_id, route_device) {
self.set_volume(device_id, props, route_device, route, volume, balance);
return;
};
}
let Some(node) = self.node(id) else {
return;
};
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamProps,
ParamType::Props,
pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, false),
pod::property!(
FormatProperties(libspa_sys::SPA_PROP_channelVolumes),
ValueArray,
pod::ValueArray::Float(volume::to_channel_volumes(
&props.channel_map.as_deref().unwrap_or_default(),
volume,
balance,
))
)
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
node.set_param(ParamType::Props, 0, param);
}
}
fn set_profile(&mut self, id: DeviceId, index: u32, save: bool) {
let Some(device) = self.device(id) else {
return;
};
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamProfile,
ParamType::Profile,
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_PROFILE_index),
Int,
index as i32
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_PROFILE_save),
Bool,
save
)
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
device.set_param(ParamType::Profile, 0, param);
}
}
fn set_volume(
&self,
id: DeviceId,
props: &NodeProps,
route_device: i32,
route: &Route,
volume: f32,
balance: Option<f32>,
) {
let Some(device) = self.device(id) else {
return;
};
let route_props = pod::object!(
SpaTypes::ObjectParamProps,
ParamType::Props,
pod::property!(FormatProperties(libspa_sys::SPA_PROP_mute), Bool, false),
pod::property!(
FormatProperties(libspa_sys::SPA_PROP_channelVolumes),
ValueArray,
pod::ValueArray::Float(if matches!(route.direction, Direction::Output) {
volume::to_channel_volumes(
&props.channel_map.as_deref().unwrap_or_default(),
volume,
balance,
)
} else {
vec![volume * volume * volume]
})
)
);
let buffer = std::io::Cursor::new(Vec::new());
let Ok(serialized) = PodSerializer::serialize(
buffer,
&pod::Value::Object(pod::object!(
SpaTypes::ObjectParamRoute,
ParamType::Route,
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_index),
Int,
route.index
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_device),
Int,
route_device
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_props),
Object,
route_props
),
pod::property!(
FormatProperties(libspa_sys::SPA_PARAM_ROUTE_save),
Bool,
true
)
)),
)
.map(|(cursor, _)| cursor.into_inner()) else {
return;
};
if let Some(param) = Pod::from_bytes(&serialized) {
device.set_param(ParamType::Route, 0, param);
}
}
fn device(&self, id: DeviceId) -> Option<&pipewire::device::Device> {
self.proxies
.devices
.values()
.find(|(device_id, ..)| id == *device_id)
.map(|(_, device, ..)| device)
}
fn node(&self, id: NodeId) -> Option<&pipewire::node::Node> {
self.proxies
.nodes
.values()
.find(|(node_id, ..)| id == *node_id)
.map(|(_, node, ..)| node)
}
}
pub struct Sender(pipewire::channel::Sender<Request>);
impl Sender {
pub fn send(&self, request: Request) -> Result<(), Request> {
self.0.send(request)
}
}
impl Drop for Sender {
fn drop(&mut self) {
_ = self.0.send(Request::Quit);
}
}
pub mod volume {
use crate::Channel;
/// Get the configured volume and balance based on a provided channel volumes array.
pub fn from_channel_volumes(channels: &[f32]) -> (f32, Option<f32>) {
let left_volume = channels.first().cloned().unwrap_or_default();
let right_volume = channels.last().cloned().unwrap_or_default();
if (left_volume - right_volume).abs() < f32::EPSILON {
return (left_volume.powf(1.0 / 3.0), None);
}
let (volume, balance) = if left_volume >= right_volume {
(left_volume, right_volume / left_volume)
} else {
(right_volume, (2.0 - (left_volume / right_volume)))
};
(volume.powf(1.0 / 3.0), Some(balance))
}
/// Create a channel volumes array based on the provided volume, balance, and channel positions.
pub fn to_channel_volumes(
channel_map: &[Channel],
volume: f32,
balance: Option<f32>,
) -> Vec<f32> {
let volume = volume * volume * volume;
if let Some(balance) = balance {
let (left_volume, right_volume) = if balance >= 1.0 {
((volume * (balance - 2.0).abs()), volume)
} else {
(volume, volume * balance)
};
let center_volume = (left_volume + right_volume) / 2.0;
let mut channel_volumes = Vec::with_capacity(channel_map.len());
// Use channel identifiers to apply volume balance
for channel in channel_map {
channel_volumes.push(match channel {
// Left channels
Channel::FL
| Channel::SL
| Channel::FLC
| Channel::RL
| Channel::TFL
| Channel::TFC
| Channel::TRL
| Channel::RLC
| Channel::FLW
| Channel::FLH
| Channel::TFLC
| Channel::TSL
| Channel::LLFE
| Channel::BLC => left_volume,
// Right channels
Channel::FR
| Channel::SR
| Channel::FRC
| Channel::RR
| Channel::TFR
| Channel::TRC
| Channel::TRR
| Channel::RRC
| Channel::FRW
| Channel::FRH
| Channel::TFRC
| Channel::TSR
| Channel::RLFE
| Channel::BRC => right_volume,
// Center/neutral channels
_ => center_volume,
});
}
channel_volumes
} else {
vec![volume; channel_map.len()]
}
}
#[cfg(test)]
mod test {
use crate::Channel;
#[test]
fn volume_balance_to_channel_volumes() {
// Test conversions to and from a channel
let channel_map = &[Channel::FL, Channel::FR];
let inputs = vec![
((0.77, Some(0.32)), &[0.45653298, 0.14609055]),
((0.77, Some(0.57)), &[0.45653298, 0.2602238]),
((0.77, Some(0.68)), &[0.45653298, 0.31044245]),
((0.77, Some(0.74)), &[0.45653298, 0.33783442]),
((0.77, Some(1.00)), &[0.45653298, 0.45653298]),
((0.77, Some(1.32)), &[0.31044242, 0.45653298]),
((0.77, Some(1.57)), &[0.19630916, 0.45653298]),
((0.77, Some(1.68)), &[0.14609058, 0.45653298]),
((0.77, Some(1.74)), &[0.118698575, 0.45653298]),
];
for ((volume, balance), channel_volumes) in inputs {
let out = super::to_channel_volumes(channel_map, volume, balance);
assert_eq!(&out, channel_volumes);
let res = super::from_channel_volumes(&out);
assert!((volume - res.0).abs() < 0.01, "{} != {}", volume, res.0);
assert!(
balance.map_or_else(
|| res.1 == Some(1.0),
|b| res.1.map_or_else(|| b == 1.0, |r| (b - r).abs() < 0.01)
),
"{:?} != {:?}",
balance,
res.1
);
}
}
}
}