feat: working version

This commit is contained in:
2024-06-28 23:55:33 +02:00
parent c2f0cd0492
commit 1f48737b2b
7 changed files with 1123 additions and 2 deletions

25
src/instructions.rs Normal file
View File

@@ -0,0 +1,25 @@
#[repr(u8)]
pub enum MixerInstruction {
SelectNext,
SelectPrevious,
ToggleMuteCurrent,
IncreaseCurrent,
DecreaseCurrent,
GetCurrent,
PlayPauseCurrent,
}
impl MixerInstruction {
pub fn from_u8(byte: u8) -> Self {
match byte {
0 => MixerInstruction::SelectNext,
1 => MixerInstruction::SelectPrevious,
2 => MixerInstruction::ToggleMuteCurrent,
3 => MixerInstruction::IncreaseCurrent,
4 => MixerInstruction::DecreaseCurrent,
5 => MixerInstruction::GetCurrent,
6 => MixerInstruction::PlayPauseCurrent,
_ => panic!("Could not parse '{byte}' to MixerInstruction"),
}
}
}

View File

@@ -1,3 +1,50 @@
fn main() {
println!("Hello, world!");
mod instructions;
pub mod mixer;
pub mod pulseaudio;
pub mod utils;
use anyhow::Result;
use mixer::Mixer;
use pulseaudio::PulseInstruction;
use std::{process::Command, sync::mpsc::channel};
const NOTIFY_SEND_REPLACE_ID: u32 = 1448531;
#[tokio::main]
async fn main() {
let mainloop = pulse::mainloop::standard::Mainloop::new().expect("Error getting main loop");
let (pulse_ix_tx, pulse_ix_rx) = channel::<PulseInstruction>();
let mut mixer = Mixer::new(mainloop, pulse_ix_tx);
mixer.run(pulse_ix_rx);
}
pub fn send_notification(message: &str) -> Result<()> {
Command::new("notify-send")
.args(vec![
"Mixrs",
message,
"-r",
&NOTIFY_SEND_REPLACE_ID.to_string(),
])
.env("DBUS_SESSION_BUS_ADDRESS", "unix:path=/run/user/1000/bus")
.spawn()?;
Ok(())
}
pub fn playerctl_toggle(target: &str) -> Result<()> {
let get_players = Command::new("playerctl").arg("-l").output()?;
let get_players_output = String::from_utf8(get_players.stdout)?;
let players: Vec<&str> = get_players_output.split("\n").collect();
match players.iter().find(|p| p.to_lowercase().contains(&target.to_lowercase())) {
Some(player) => {
Command::new("playerctl").args(vec!["-p", player, "play-pause"]).spawn()?;
},
None => {},
};
Ok(())
}

465
src/mixer.rs Normal file
View File

@@ -0,0 +1,465 @@
use anyhow::Result;
use std::{
borrow::{Borrow, BorrowMut},
collections::HashMap,
fs,
io::Read,
os::unix::net::UnixListener,
path::Path,
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex,
},
thread,
time::Duration,
usize,
};
use pulse::{
callbacks::ListResult,
context::{
subscribe::{Facility, InterestMaskSet, Operation},
FlagSet,
},
mainloop::standard::{IterateResult, Mainloop},
volume::ChannelVolumes,
};
use crate::{
instructions::MixerInstruction,
playerctl_toggle,
pulseaudio::{PulseInstruction, SinkInputMixerData},
send_notification,
utils::{get_sink_input_name, percentage_to_total_volume},
};
pub struct Mixer {
sink_inputs: HashMap<u32, SinkInputMixerData>,
selected_index: Arc<Mutex<Option<usize>>>,
mainloop: Mainloop,
context: pulse::context::Context,
}
impl Mixer {
pub fn new(mut mainloop: Mainloop, pulse_ix_tx: Sender<PulseInstruction>) -> Self {
let mut context =
pulse::context::Context::new(&mainloop, "Mixrs").expect("Error creating pulse context");
context
.borrow_mut()
.connect(None, FlagSet::NOFLAGS, None)
.expect("Error connecting pulse context");
loop {
match mainloop.borrow_mut().iterate(false) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
panic!("Iterate state was not success, quitting...");
}
IterateResult::Success(_) => {}
}
match context.borrow().get_state() {
pulse::context::State::Ready => {
break;
}
pulse::context::State::Failed | pulse::context::State::Terminated => {
panic!("Context state failed/terminated, quitting...");
}
_ => {}
}
}
let sink_inputs: HashMap<u32, SinkInputMixerData> = HashMap::new();
let selected_index: Arc<Mutex<Option<usize>>> = Arc::new(Mutex::new(None));
context.subscribe(InterestMaskSet::SINK_INPUT, |_| {});
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
let Some(facility) = facility else {
return;
};
let Some(operation) = operation else {
return;
};
let Facility::SinkInput = facility else {
return;
};
pulse_ix_tx
.send(match operation {
Operation::New => PulseInstruction::AddSinkInput(index),
Operation::Changed => PulseInstruction::UpdateSinkInput(index),
Operation::Removed => PulseInstruction::RemoveSinkInput(index),
})
.unwrap();
})));
Self {
sink_inputs,
selected_index,
mainloop,
context,
}
}
pub fn create_socket_listener(&self) -> Result<UnixListener> {
let socket_path = Path::new("/tmp/mixrs");
if socket_path.exists() {
fs::remove_file(socket_path)?;
}
let listener = UnixListener::bind(socket_path)?;
Ok(listener)
}
pub fn run(&mut self, pulse_ix_rx: Receiver<PulseInstruction>) -> ! {
let listener = self
.create_socket_listener()
.expect("Error creating unix socket listener");
let (mixer_tx, mixer_rx) = channel::<MixerInstruction>();
thread::spawn(move || {
for client in listener.incoming() {
match client {
Ok(mut stream) => {
let mut buf: Vec<u8> = Vec::with_capacity(1);
stream.read_to_end(&mut buf).expect("Error reading stream");
mixer_tx.send(MixerInstruction::from_u8(buf[0])).unwrap();
}
Err(_) => println!("Stream error"),
}
}
});
let initial_sink_inputs: Arc<Mutex<HashMap<u32, SinkInputMixerData>>> =
Arc::new(Mutex::new(HashMap::new()));
let callback_initial_sink_inputs = initial_sink_inputs.clone();
let initial_sink_inputs_operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info_list(move |r| {
let ListResult::Item(sink_input) = r else {
return;
};
callback_initial_sink_inputs.lock().unwrap().insert(
sink_input.index,
SinkInputMixerData {
name: get_sink_input_name(&sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
},
);
});
while initial_sink_inputs_operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
self.sink_inputs = initial_sink_inputs.lock().unwrap().clone();
*self.selected_index.lock().unwrap() = match self.sink_inputs.keys().nth(0) {
Some(_) => Some(0),
None => None,
};
loop {
match mixer_rx.try_recv() {
Ok(ix) => match ix {
MixerInstruction::SelectNext => self.select_next(),
MixerInstruction::SelectPrevious => self.select_previous(),
MixerInstruction::ToggleMuteCurrent => self.toggle_mute_current(),
MixerInstruction::IncreaseCurrent => self.increase_volume_current(),
MixerInstruction::DecreaseCurrent => self.decrease_volume_current(),
MixerInstruction::GetCurrent => self.get_current(),
MixerInstruction::PlayPauseCurrent => self.play_pause_current(),
},
Err(_) => (),
}
if let Some(ix) = pulse_ix_rx.try_recv().ok() {
match ix {
PulseInstruction::AddSinkInput(sink_index) => {
let result: Arc<Mutex<Option<SinkInputMixerData>>> =
Arc::new(Mutex::new(None));
let operation_result = result.clone();
let operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info(sink_index, move |r| {
if let ListResult::Item(sink_input) = r {
*operation_result.lock().unwrap() = Some(SinkInputMixerData {
name: get_sink_input_name(sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
});
}
});
while operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
let sink_input = result.lock().unwrap().take();
if let Some(sink_input) = sink_input {
self.sink_inputs.insert(sink_index, sink_input);
}
}
PulseInstruction::RemoveSinkInput(sink_index) => {
let selected_index_lock = self.selected_index.lock().unwrap();
match *selected_index_lock {
Some(current_index) => {
drop(selected_index_lock);
let removed_sink_input_index = self
.sink_inputs
.keys()
.position(|k| *k == sink_index)
.unwrap();
let current_key =
*self.sink_inputs.keys().nth(current_index).unwrap();
if self.sink_inputs.remove(&sink_index).is_some() {
if sink_index == current_key
|| removed_sink_input_index > current_index
{
self.select_previous();
}
}
}
None => (),
}
}
PulseInstruction::UpdateSinkInput(sink_index) => {
match self.sink_inputs.get_mut(&sink_index) {
Some(sink_input_mixer_data) => {
let new_sink_input: Arc<Mutex<Option<SinkInputMixerData>>> =
Arc::new(Mutex::new(None));
let callback_new_sink_input = new_sink_input.clone();
let operation = self
.context
.borrow_mut()
.introspect()
.borrow_mut()
.get_sink_input_info(sink_index, move |r| {
let ListResult::Item(sink_input) = r else {
return;
};
*callback_new_sink_input.lock().unwrap() =
Some(SinkInputMixerData {
name: get_sink_input_name(&sink_input).unwrap(),
volume: sink_input.volume.avg().0,
channels: sink_input.volume.len(),
muted: sink_input.mute,
});
});
while operation.get_state() == pulse::operation::State::Running {
iterate_mainloop(&mut self.mainloop);
}
let mut sink_input_lock = new_sink_input.lock().unwrap();
if let Some(new_sink_input) = sink_input_lock.take() {
*sink_input_mixer_data = new_sink_input;
}
}
None => (),
}
}
}
}
iterate_mainloop(&mut self.mainloop);
}
}
pub fn select_next(&mut self) {
let mut index_lock = self.selected_index.lock().unwrap();
match *index_lock {
Some(current_index) => {
let new_index: usize =
(current_index.overflowing_add(1).0 % self.sink_inputs.len()).max(0);
if current_index != new_index {
*index_lock = Some(new_index);
}
drop(index_lock);
self.get_current();
}
None => {
*index_lock = if self.sink_inputs.len() > 0 {
Some(0)
} else {
None
};
}
}
}
pub fn select_previous(&mut self) {
let mut index_lock = self.selected_index.lock().unwrap();
match *index_lock {
Some(current_index) => {
let new_index: usize = match current_index.overflowing_sub(1) {
(_, true) => self.sink_inputs.len() - 1,
(new_value, false) => new_value,
};
if current_index != new_index {
*index_lock = Some(new_index);
}
drop(index_lock);
self.get_current();
}
None => {
*index_lock = if self.sink_inputs.len() > 0 {
Some(0)
} else {
None
};
}
}
}
pub fn toggle_mute_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_mute(
sink_index,
!self.sink_inputs.get(&sink_index).unwrap().muted,
None,
);
}
pub fn increase_volume_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
let mut volume = ChannelVolumes::default();
volume.set(
sink_input.channels,
pulse::volume::Volume(sink_input.volume),
);
volume.increase(pulse::volume::Volume(percentage_to_total_volume(5)));
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_volume(sink_index, &volume, None);
}
pub fn decrease_volume_current(&mut self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let sink_input = self.sink_inputs.get(&sink_index).unwrap();
let mut volume = ChannelVolumes::default();
volume.set(
sink_input.channels,
pulse::volume::Volume(sink_input.volume),
);
volume.decrease(pulse::volume::Volume(percentage_to_total_volume(5)));
self.context
.borrow_mut()
.introspect()
.borrow_mut()
.set_sink_input_volume(sink_index, &volume, None);
}
pub fn get_current(&self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
let _ = send_notification(&format!("Selected: {current_name}"));
}
pub fn play_pause_current(&self) {
let index_lock = self.selected_index.lock().unwrap();
let Some(index) = *index_lock else {
return;
};
drop(index_lock);
let sink_index = *self.sink_inputs.keys().nth(index).unwrap();
let current_name = &self.sink_inputs.get(&sink_index).unwrap().name;
match playerctl_toggle(current_name) {
Ok(_) => {
let _ = send_notification(&format!("Toggled {current_name}"));
}
Err(_) => (),
};
}
}
pub fn iterate_mainloop(mainloop: &mut pulse::mainloop::standard::Mainloop) {
mainloop.borrow_mut().iterate(false);
thread::sleep(Duration::from_millis(5));
}

32
src/pulseaudio/mod.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::u32;
use crate::utils::total_volume_to_percentage;
pub enum PulseInstruction {
AddSinkInput(u32),
RemoveSinkInput(u32),
UpdateSinkInput(u32),
}
pub enum PulseResponse {
Ok,
Error,
SinkInput(Option<SinkInputMixerData>),
SinkInputs(Vec<SinkInputMixerData>),
}
#[derive(Clone, Debug)]
pub struct SinkInputMixerData {
/// The input sink's `application.name`
pub name: String,
/// The input sink's volume
pub volume: u32,
pub muted: bool,
pub channels: u8,
}
impl SinkInputMixerData {
pub fn get_volume_percent(&self) -> u8 {
total_volume_to_percentage(self.volume)
}
}

28
src/utils.rs Normal file
View File

@@ -0,0 +1,28 @@
use anyhow::anyhow;
use pulse::{context::introspect::SinkInputInfo, volume};
const FULL_VOLUME: u32 = 1 << 16;
pub fn volume_to_percentage(volume: volume::ChannelVolumes) -> u8 {
let average = volume.avg().0;
total_volume_to_percentage(average)
}
pub fn total_volume_to_percentage(volume: u32) -> u8 {
((volume as f32 / FULL_VOLUME as f32) * 100.0).round() as u8
}
pub fn percentage_to_total_volume(percentage: u8) -> u32 {
((FULL_VOLUME as f32 / 100.0) * percentage as f32).round() as u32
}
pub fn get_sink_input_name(sink_input: &SinkInputInfo) -> anyhow::Result<String> {
let Some(name_bytes) = sink_input.proplist.get("application.name") else {
return Err(anyhow!("Invalid sink input name"));
};
Ok(String::from_utf8(
name_bytes[..name_bytes.len() - 1].to_vec(),
)?)
}