Start setting up the audio control system

This commit is contained in:
Savanni D'Gerinel 2024-08-28 23:45:34 -04:00
parent 7467e8d5b2
commit 281bef855b
3 changed files with 187 additions and 26 deletions

View File

@ -1,27 +1,44 @@
use std::{collections::HashSet, sync::{Arc, RwLock}}; use crate::audio_control::AudioControl;
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
struct State_ { struct AppState {
device_list: Vec<String>, device_list: Vec<String>,
track_list: Vec<String>, track_list: Vec<String>,
currently_playing: HashSet<String>, currently_playing: HashSet<String>,
audio_control: AudioControl,
} }
#[derive(Clone)] impl Default for AppState {
pub struct State { fn default() -> Self {
internal: Arc<RwLock<State_>>, Self {
}
impl State {
fn new() -> State {
let internal = State_ {
device_list: vec![], device_list: vec![],
track_list: vec![ track_list: vec![
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3".to_owned(), "/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3".to_owned(),
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/02 - Against the Clock.mp3.mp3".to_owned(),
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/03 - Alleyway Cutthroat.mp3.mp3".to_owned(),
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/04 - Beasts Of Legend.mp3.mp3".to_owned(),
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3".to_owned(), "/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3".to_owned(),
], ],
currently_playing: HashSet::default(), currently_playing: HashSet::default(),
};
State { audio_control: AudioControl::default(),
}
}
}
#[derive(Clone)]
pub struct App {
internal: Arc<RwLock<AppState>>,
}
impl App {
fn new() -> App {
let internal = AppState::default();
App {
internal: Arc::new(RwLock::new(internal)), internal: Arc::new(RwLock::new(internal)),
} }
} }
@ -41,11 +58,17 @@ impl State {
st.track_list.clone() st.track_list.clone()
} }
pub fn play(&self, track: String) -> Result<(), String> { pub fn play_pause(&self) -> Result<(), String> {
self.internal.write().unwrap().audio_control.play_pause();
Ok(())
}
pub fn add_track(&self, track: String) -> Result<(), String> {
let mut st = self.internal.write().unwrap(); let mut st = self.internal.write().unwrap();
if st.track_list.contains(&track) { if st.track_list.contains(&track) {
st.currently_playing.insert(track); st.currently_playing.insert(track.clone());
} }
st.audio_control.add_track(track);
Ok(()) Ok(())
} }
@ -67,10 +90,8 @@ impl State {
} }
} }
impl Default for State { impl Default for App {
fn default() -> State { fn default() -> App {
State::new() App::new()
} }
} }

View File

@ -0,0 +1,138 @@
use gstreamer::{prelude::*, ClockTime, MessageType, MessageView};
use std::sync::{Arc, RwLock};
pub struct AudioControl {
bus: gstreamer::Bus,
pipeline: gstreamer::Pipeline,
mixer: gstreamer::Element,
audio_sink: gstreamer::Element,
bus_monitor: std::thread::JoinHandle<()>,
playing: Arc<RwLock<bool>>,
}
impl Default for AudioControl {
fn default() -> Self {
let pipeline = gstreamer::Pipeline::new();
let bus = pipeline.bus().unwrap();
let mixer = gstreamer::ElementFactory::find("audiomixer")
.unwrap()
.load()
.unwrap()
.create()
.build()
.unwrap();
pipeline.add(&mixer).unwrap();
let audio_sink = gstreamer::ElementFactory::find("pulsesink")
.unwrap()
.load()
.unwrap()
.create()
.build()
.unwrap();
pipeline.add(&audio_sink).unwrap();
mixer.link(&audio_sink).unwrap();
let playing = Arc::new(RwLock::new(false));
let bus_monitor = std::thread::spawn({
let pipeline_object = pipeline.clone().upcast::<gstreamer::Object>();
let playing = playing.clone();
let bus = bus.clone();
move || loop {
if let Some(msg) = bus.timed_pop_filtered(
ClockTime::NONE,
&[
MessageType::Error,
MessageType::Eos,
MessageType::StateChanged,
],
) {
match msg.view() {
MessageView::StateChanged(st) => {
if msg.src() == Some(&pipeline_object) {
*playing.write().unwrap() = st.current() == gstreamer::State::Playing;
}
}
MessageView::Error(err) => {
println!("error: {:?}", err);
}
MessageView::Eos(_) => {
println!("EOS");
}
_ => {
unreachable!();
}
}
}
}
});
Self {
bus,
pipeline,
mixer,
audio_sink,
bus_monitor,
playing,
}
}
}
impl AudioControl {
pub fn play_pause(&self) {
if *self.playing.read().unwrap() {
self.pipeline.set_state(gstreamer::State::Paused).unwrap();
} else {
self.pipeline.set_state(gstreamer::State::Playing).unwrap();
}
}
pub fn add_track(&mut self, path: String) {
let source = gstreamer::ElementFactory::find("filesrc")
.unwrap()
.load()
.unwrap()
.create()
.property("location", path)
.build()
.unwrap();
self.pipeline.add(&source).unwrap();
let decoder = gstreamer::ElementFactory::find("decodebin")
.unwrap()
.load()
.unwrap()
.create()
.build()
.unwrap();
self.pipeline.add(&decoder).unwrap();
source.link(&decoder).unwrap();
let volume = gstreamer::ElementFactory::find("volume")
.unwrap()
.load()
.unwrap()
.create()
.property("mute", false)
.property("volume", 0.75)
.build()
.unwrap();
self.pipeline.add(&volume).unwrap();
volume.link(&self.mixer).unwrap();
decoder.connect_pad_added(move |_, pad| {
let next_pad = volume.static_pad("sink").unwrap();
pad.link(&next_pad).unwrap();
});
}
pub fn remove_track(&mut self, path: String) {
/* Need to run EOS through to a probe on the trailing end of the volume element */
}
}

View File

@ -4,15 +4,17 @@ use std::net::{Ipv6Addr, SocketAddrV6};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use warp::{serve, Filter}; use warp::{serve, Filter};
mod state; mod audio_control;
use state::State;
mod app;
use app::App;
#[derive(Deserialize)] #[derive(Deserialize)]
struct PlayTrackParams { struct PlayTrackParams {
track_name: String, track_name: String,
} }
async fn server_main(state: State) { async fn server_main(state: App) {
let localhost: Ipv6Addr = "::1".parse().unwrap(); let localhost: Ipv6Addr = "::1".parse().unwrap();
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0); let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
@ -76,7 +78,7 @@ async fn server_main(state: State) {
serve(routes).run(server_addr).await; serve(routes).run(server_addr).await;
} }
fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::DictRef) { fn handle_add_audio_device(state: App, props: &pipewire::spa::utils::dict::DictRef) {
if props.get("media.class") == Some("Audio/Sink") { if props.get("media.class") == Some("Audio/Sink") {
if let Some(device_name) = props.get("node.description") { if let Some(device_name) = props.get("node.description") {
state.add_audio(device_name.to_owned()); state.add_audio(device_name.to_owned());
@ -84,7 +86,7 @@ fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::Dic
} }
} }
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> { fn pipewire_loop(state: App) -> Result<(), Box<dyn std::error::Error>> {
let mainloop = MainLoop::new(None)?; let mainloop = MainLoop::new(None)?;
let context = Context::new(&mainloop)?; let context = Context::new(&mainloop)?;
let core = context.connect(None)?; let core = context.connect(None)?;
@ -107,13 +109,13 @@ fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn pipewire_main(state: State) { fn pipewire_main(state: App) {
pipewire_loop(state).expect("pipewire should not error"); pipewire_loop(state).expect("pipewire should not error");
} }
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let state = State::default(); let state = App::default();
spawn_blocking({ spawn_blocking({
let state = state.clone(); let state = state.clone();