Compare commits

..

No commits in common. "7da7ffcaa503303e4490f35543633d04fe6e3063" and "281bef855bc68742a2cade24eff9f9ac7c61480b" have entirely different histories.

9 changed files with 280 additions and 864 deletions

2
Cargo.lock generated
View File

@ -1946,13 +1946,11 @@ dependencies = [
name = "gm-dash" name = "gm-dash"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cool_asserts",
"glib 0.18.5", "glib 0.18.5",
"gstreamer", "gstreamer",
"pipewire", "pipewire",
"serde 1.0.209", "serde 1.0.209",
"serde_json", "serde_json",
"thiserror",
"tokio", "tokio",
"warp", "warp",
] ]

View File

@ -13,6 +13,3 @@ serde_json = { version = "1.0.127" }
tokio = { version = "1.39.3", features = ["full"] } tokio = { version = "1.39.3", features = ["full"] }
warp = { version = "0.3.7" } warp = { version = "0.3.7" }
glib = { version = "0.18" } glib = { version = "0.18" }
thiserror = "1.0.63"
cool_asserts = "2.0.3"

97
gm-dash/server/src/app.rs Normal file
View File

@ -0,0 +1,97 @@
use crate::audio_control::AudioControl;
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
struct AppState {
device_list: Vec<String>,
track_list: Vec<String>,
currently_playing: HashSet<String>,
audio_control: AudioControl,
}
impl Default for AppState {
fn default() -> Self {
Self {
device_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/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(),
],
currently_playing: HashSet::default(),
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)),
}
}
pub fn add_audio(&self, device: String) {
let mut st = self.internal.write().unwrap();
st.device_list.push(device);
}
pub fn audio_devices(&self) -> Vec<String> {
let st = self.internal.read().unwrap();
st.device_list.clone()
}
pub fn tracks(&self) -> Vec<String> {
let st = self.internal.read().unwrap();
st.track_list.clone()
}
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();
if st.track_list.contains(&track) {
st.currently_playing.insert(track.clone());
}
st.audio_control.add_track(track);
Ok(())
}
pub fn stop(&self, track: String) -> Result<(), String> {
let mut st = self.internal.write().unwrap();
st.currently_playing.remove(&track);
Ok(())
}
pub fn stop_all(&self) -> Result<(), String> {
let mut st = self.internal.write().unwrap();
st.currently_playing = HashSet::new();
Ok(())
}
pub fn playing(&self) -> Vec<String> {
let st = self.internal.read().unwrap();
st.currently_playing.iter().cloned().collect()
}
}
impl Default for App {
fn default() -> App {
App::new()
}
}

View File

@ -1,163 +0,0 @@
use std::{
collections::HashMap,
path::PathBuf,
sync::{Arc, RwLock},
};
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
audio_control::AudioControl,
types::{
AppError, AudioControlMessage, AudioState, AudioStatusMessage, TrackInfo, TrackSpec, Volume,
},
};
#[cfg(test)]
mod tests;
struct AppState {
playing: bool,
device_list: Vec<String>,
track_list: Vec<PathBuf>,
track_status: Vec<TrackInfo>,
}
impl Default for AppState {
fn default() -> Self {
Self {
playing: false,
device_list: vec![],
track_list: vec![],
track_status: vec![],
}
}
}
pub struct App {
state: Arc<RwLock<AppState>>,
audio_control: Sender<AudioControlMessage>,
listener: JoinHandle<()>,
}
impl App {
pub fn new(
audio_control: Sender<AudioControlMessage>,
mut audio_status: Receiver<AudioStatusMessage>,
) -> App {
let state = Arc::new(RwLock::new(AppState::default()));
let listener = tokio::spawn({
let state = state.clone();
async move {
println!("listener started");
while let Some(msg) = audio_status.recv().await {
match msg {
AudioStatusMessage::Playing => {
state.write().unwrap().playing = true;
}
AudioStatusMessage::Status(AudioState {
playing: _playing,
tracks,
}) => {
state.write().unwrap().track_status = tracks;
}
msg => println!("message received from audio controller: {:?}", msg),
}
}
println!("listener exiting");
}
});
Self {
state,
audio_control,
listener,
}
}
pub fn playing(&self) -> bool {
self.state.read().unwrap().playing
}
pub fn add_audio(&self, device: String) {
/*
let mut st = self.internal.write().unwrap();
st.device_list.push(device);
*/
}
pub fn audio_devices(&self) -> Vec<String> {
/*
let st = self.internal.read().unwrap();
st.device_list.clone()
*/
vec![]
}
pub fn enabled_tracks(&self) -> Vec<PathBuf> {
let st = self.state.read().unwrap();
st.track_status.iter().map(|ti| ti.path.clone()).collect()
}
pub async fn enable_track(&self, path: PathBuf) -> Result<(), AppError> {
println!("enabling track: {}", path.display());
self.audio_control
.send(AudioControlMessage::EnableTrack(TrackSpec {
path,
volume: Volume::try_from(1.0).unwrap(),
}))
.await
.expect("audio control send to succeed");
/*
let st = self.internal.write().unwrap();
st.audio_control.add_track(TrackSpec{ path })?;
*/
Ok(())
}
pub async fn disable_track(&self, _track: &str) -> Result<(), AppError> {
/*
let mut st = self.internal.write().unwrap();
if st.currently_playing.contains_key(track) {
st.currently_playing.remove(track);
}
Ok(())
*/
Ok(())
}
pub async fn play(&self) -> Result<(), AppError> {
self.audio_control
.send(AudioControlMessage::Play)
.await
.expect("audio control send to succeed");
Ok(())
}
pub async fn stop(&self) -> Result<(), AppError> {
self.audio_control
.send(AudioControlMessage::Stop)
.await
.expect("audio control send to succeed");
Ok(())
}
pub async fn pause(&self) -> Result<(), AppError> {
self.audio_control
.send(AudioControlMessage::Pause)
.await
.expect("audio control send to succeed");
Ok(())
}
}

View File

@ -1,126 +0,0 @@
use std::{future::Future, path::PathBuf, time::Duration};
use cool_asserts::assert_matches;
use tokio::sync::mpsc::{Receiver, Sender};
use crate::{
app::{App, AppError},
types::{AudioControlMessage, AudioStatusMessage, Progress, TrackInfo, TrackSpec, Volume},
};
fn memory_app() -> (
App,
Receiver<AudioControlMessage>,
Sender<AudioStatusMessage>,
) {
let (audio_control_tx, audio_control_rx) = tokio::sync::mpsc::channel(5);
let (audio_status_tx, audio_status_rx) = tokio::sync::mpsc::channel(5);
let app = App::new(audio_control_tx, audio_status_rx);
(app, audio_control_rx, audio_status_tx)
}
#[tokio::test]
async fn app_starts_in_stopped_state() {
let (app, _control_rx, _status_tx) = memory_app();
assert!(!app.playing());
}
#[tokio::test]
async fn can_add_a_track_without_starting_playback() {
let (app, mut control_rx, status_tx) = memory_app();
let path_1 = PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3");
let path_2 = PathBuf::from(
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
);
{
app.enable_track(path_1.clone())
.await
.expect("to enable a track");
assert_matches!(control_rx.recv().await, Some(AudioControlMessage::EnableTrack(trackspec)) => {
assert_eq!(trackspec, TrackSpec{ path: path_1.clone(), volume: Volume::try_from(1.0).unwrap() });
});
status_tx
.send(AudioStatusMessage::Status(vec![TrackInfo {
path: path_1.clone(),
volume: Volume::try_from(1.0).unwrap(),
progress: Progress {
current: Duration::from_secs(0),
length: Duration::from_secs(100),
},
}]))
.await
.expect("status send to work");
tokio::time::sleep(Duration::from_millis(1)).await;
let tracks = app.enabled_tracks();
tracks.iter().find(|p| **p == path_1);
assert!(!app.playing());
}
{
app.enable_track(path_2.clone())
.await
.expect("to enable a track");
assert_matches!(control_rx.recv().await, Some(AudioControlMessage::EnableTrack(trackspec)) => {
assert_eq!(trackspec, TrackSpec{ path: path_2.clone(), volume: Volume::try_from(1.0).unwrap() });
});
status_tx
.send(AudioStatusMessage::Status(vec![
TrackInfo {
path: path_1.clone(),
volume: Volume::try_from(1.0).unwrap(),
progress: Progress {
current: Duration::from_secs(0),
length: Duration::from_secs(100),
},
},
TrackInfo {
path: path_2.clone(),
volume: Volume::try_from(1.0).unwrap(),
progress: Progress {
current: Duration::from_secs(0),
length: Duration::from_secs(100),
},
},
]))
.await
.expect("status send to work");
tokio::time::sleep(Duration::from_millis(1)).await;
let tracks = app.enabled_tracks();
tracks.iter().find(|p| **p == path_1);
tracks.iter().find(|p| **p == path_2);
}
}
#[tokio::test]
async fn cannot_start_playback_with_no_tracks() {
let (app, control_rx, status_tx) = memory_app();
// assert_matches!(app.play(), Err(AppError::NoTracks));
unimplemented!()
}
#[tokio::test]
async fn can_add_a_track_during_playback() {
let (app, control_rx, status_tx) = memory_app();
/*
app.enable_track(PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3")).expect("to enable a track");
app.play().expect("to start playback");
assert!(app.playing());
app.enable_track(PathBuf::from(
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
))
.expect("to enable another track during playback");
*/
unimplemented!()
}

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

@ -1,368 +0,0 @@
use std::{
collections::HashMap,
path::PathBuf,
sync::{Arc, RwLock},
time::Duration,
};
use gstreamer::{prelude::*, ClockTime, MessageType, MessageView};
use thiserror::Error;
use tokio::sync::mpsc::{Receiver, Sender};
use crate::types::{AudioControlMessage, AudioState, AudioStatusMessage, Progress, TrackInfo, TrackSpec};
#[derive(Debug, Error, PartialEq)]
pub enum AudioError {
#[error("No tracks are available to play")]
NoTracks,
#[error("Cannot perform operation in the current state")]
InvalidState,
}
pub struct AudioControl {
backend: Box<dyn AudioControlBackend>,
}
impl AudioControl {
pub fn new(backend: impl AudioControlBackend + 'static) -> Self {
Self {
backend: Box::new(backend),
}
}
pub async fn listen(&self, mut control_rx: Receiver<AudioControlMessage>) {
while let Some(msg) = control_rx.recv().await {
println!("control message: {:?}", msg);
match msg {
AudioControlMessage::Play => {
self.backend.play().unwrap();
}
AudioControlMessage::Stop => {
self.backend.stop().unwrap();
}
AudioControlMessage::Pause => {
self.backend.pause().unwrap();
}
AudioControlMessage::EnableTrack(spec) => {
self.backend.add_track(spec).unwrap();
}
AudioControlMessage::DisableTrack(_) => {
unimplemented!()
}
AudioControlMessage::ReportStatus => {
unimplemented!()
}
}
}
}
pub async fn report(&self, status_tx: Sender<AudioStatusMessage>) {
loop {
status_tx
.send(AudioStatusMessage::Status(AudioState {
playing: self.backend.playing(),
tracks: self.backend.tracks()
}))
.await
.expect("to successfully send a message");
let _ = tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
/*
pub fn playing(&self) -> bool {
self.backend.read().unwrap().playing()
}
pub fn tracks(&self) -> Vec<TrackSpec> {
self.backend.read().unwrap().tracks()
}
pub fn play(&self) -> Result<(), AudioError> {
self.backend.read().unwrap().play()
}
pub fn stop(&self) -> Result<(), AudioError> {
self.backend.read().unwrap().stop()
}
pub fn add_track(&self, track: TrackSpec) -> Result<(), AudioError> {
self.backend.write().unwrap().add_track(track)
}
}
*/
pub trait AudioControlBackend: Send + Sync {
fn playing(&self) -> bool;
fn tracks(&self) -> Vec<TrackInfo>;
fn play(&self) -> Result<(), AudioError>;
fn stop(&self) -> Result<(), AudioError>;
fn pause(&self) -> Result<(), AudioError>;
fn add_track(&self, track: TrackSpec) -> Result<(), AudioError>;
fn remove_track(&self, track: TrackSpec) -> Result<(), AudioError>;
}
/*
pub struct MemoryBackend {
playing: Arc<RwLock<bool>>,
tracks: HashMap<PathBuf, TrackSpec>,
}
impl Default for MemoryBackend {
fn default() -> Self {
Self {
playing: Arc::new(RwLock::new(false)),
tracks: HashMap::new(),
}
}
}
impl AudioControlBackend for MemoryBackend {
fn playing(&self) -> bool {
*self.playing.read().unwrap()
}
fn tracks(&self) -> Vec<TrackSpec> {
/*
self.tracks.iter().cloned().collect()
*/
vec![]
}
fn play(&self) -> Result<(), AudioError> {
if self.tracks.is_empty() {
return Err(AudioError::NoTracks);
}
let mut playing = self.playing.write().unwrap();
if *playing {
return Err(AudioError::InvalidState);
}
*playing = true;
Ok(())
}
fn stop(&self) -> Result<(), AudioError> {
let mut playing = self.playing.write().unwrap();
if *playing {
*playing = false;
Ok(())
} else {
Err(AudioError::InvalidState)
}
}
fn add_track(&mut self, track: TrackSpec) -> Result<(), AudioError> {
/*
self.tracks.insert(track);
*/
Ok(())
}
fn remove_track(&mut self, track: TrackSpec) -> Result<(), AudioError> {
/*
self.tracks.remove(&track);
*/
Ok(())
}
}
*/
struct GStreamerBackendState {
playing: bool,
tracks: HashMap<PathBuf, TrackInfo>,
}
impl Default for GStreamerBackendState {
fn default() -> Self {
Self {
playing: false,
tracks: HashMap::new(),
}
}
}
pub struct GStreamerBackend {
bus: gstreamer::Bus,
pipeline: gstreamer::Pipeline,
mixer: gstreamer::Element,
audio_sink: gstreamer::Element,
monitor: std::thread::JoinHandle<()>,
state: Arc<RwLock<GStreamerBackendState>>,
}
impl Default for GStreamerBackend {
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 state = Arc::new(RwLock::new(GStreamerBackendState::default()));
let monitor = std::thread::spawn({
let pipeline_object = pipeline.clone().upcast::<gstreamer::Object>();
let state = state.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) {
state.write().unwrap().playing =
st.current() == gstreamer::State::Playing;
}
}
MessageView::Error(err) => {
println!("error: {:?}", err);
}
MessageView::Eos(_) => {
println!("EOS");
}
_ => {
unreachable!();
}
}
}
}
});
Self {
bus,
pipeline,
mixer,
audio_sink,
monitor,
state,
}
}
}
impl AudioControlBackend for GStreamerBackend {
fn playing(&self) -> bool {
self.state.read().unwrap().playing
}
fn tracks(&self) -> Vec<TrackInfo> {
vec![]
}
fn play(&self) -> Result<(), AudioError> {
if !self.playing() {
self.pipeline.set_state(gstreamer::State::Playing).unwrap();
Ok(())
} else {
Err(AudioError::InvalidState)
}
}
fn stop(&self) -> Result<(), AudioError> {
if self.playing() {
self.pipeline.set_state(gstreamer::State::Ready).unwrap();
Ok(())
} else {
Err(AudioError::InvalidState)
}
}
fn pause(&self) -> Result<(), AudioError> {
if self.playing() {
self.pipeline.set_state(gstreamer::State::Paused).unwrap();
Ok(())
} else {
Err(AudioError::InvalidState)
}
}
fn add_track(&self, track: TrackSpec) -> Result<(), AudioError> {
let mut st = self.state.write().unwrap();
st.tracks.insert(track.path.clone(), TrackInfo {
path: track.path.clone(),
volume: track.volume,
progress: Progress {
current: Duration::from_secs(0),
length: Duration::from_secs(1),
},
});
let source = gstreamer::ElementFactory::find("filesrc")
.unwrap()
.load()
.unwrap()
.create()
.property("location", track.path.to_str().unwrap())
.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", track.volume.as_f64())
.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();
});
Ok(())
}
fn remove_track(&self, _path: TrackSpec) -> Result<(), AudioError> {
unimplemented!()
/* Need to run EOS through to a probe on the trailing end of the volume element */
}
}

View File

@ -1,128 +1,92 @@
use std::{
convert::Infallible,
net::{Ipv6Addr, SocketAddrV6},
path::PathBuf,
sync::Arc,
};
use app::App;
use audio_control::{AudioControl, GStreamerBackend};
use pipewire::{context::Context, main_loop::MainLoop}; use pipewire::{context::Context, main_loop::MainLoop};
use serde::Deserialize; use serde::Deserialize;
use std::net::{Ipv6Addr, SocketAddrV6};
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use warp::{serve, Filter}; use warp::{serve, Filter};
mod app;
mod audio_control; mod audio_control;
mod types;
mod app;
use app::App;
#[derive(Deserialize)] #[derive(Deserialize)]
struct PlayTrackParams { struct PlayTrackParams {
track_name: String, track_name: String,
} }
fn with_app(app: Arc<App>) -> impl Filter<Extract = (Arc<App>,), Error = Infallible> + Clone { async fn server_main(state: App) {
warp::any().map(move || app.clone())
}
async fn server_main(app: Arc<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);
let root = warp::path!().map(|| "ok".to_string()); let root = warp::path!().map(|| "ok".to_string());
let list_output_devices = warp::path!("output_devices").map({ let list_output_devices = warp::path!("output_devices").map({
let app = app.clone(); let state = state.clone();
move || { move || {
let devices = app.audio_devices(); let devices = state.audio_devices();
serde_json::to_string(&devices).unwrap() serde_json::to_string(&devices).unwrap()
} }
}); });
/*
let list_tracks = warp::path!("tracks").map({ let list_tracks = warp::path!("tracks").map({
let app = app.clone(); let state = state.clone();
move || serde_json::to_string(&app.tracks()).unwrap() move || serde_json::to_string(&state.tracks()).unwrap()
}); });
*/
let enable_track = warp::put() let play_track = warp::put()
.and(warp::path!("playing")) .and(warp::path!("playing"))
.and(warp::body::json()) .and(warp::body::json())
.and(with_app(app.clone())) .map({
.then(|params: PlayTrackParams, app: Arc<App>| async move { let state = state.clone();
println!("enable track"); move |params: PlayTrackParams| {
let _ = app.enable_track(PathBuf::from(params.track_name)).await; state.play(params.track_name);
"".to_owned()
});
let disable_track = warp::delete()
.and(warp::path!("playing"))
.and(warp::body::json())
.and(with_app(app.clone()))
.then(|params: PlayTrackParams, app: Arc<App>| async move {
let _ = app.disable_track(&params.track_name);
"".to_owned()
});
let play_all = warp::post()
.and(warp::path!("play"))
.and(with_app(app.clone()))
.then({
|app: Arc<App>| async move {
println!("play_all");
let _ = app.play().await;
"".to_owned() "".to_owned()
} }
}); });
let stop_all = warp::post() let stop_track = warp::delete()
.and(warp::path!("stop")) .and(warp::path!("playing"))
.and(with_app(app.clone())) .and(warp::body::json())
.then({ .map({
|app: Arc<App>| async move { let state = state.clone();
let _ = app.stop().await; move |params: PlayTrackParams| {
state.stop(params.track_name);
"".to_owned() "".to_owned()
} }
}); });
let pause = warp::post() let stop_all_tracks = warp::delete().and(warp::path!("playing")).map({
.and(warp::path!("pause")) let state = state.clone();
.and(with_app(app.clone())) move || {
.then({ state.stop_all();
|app: Arc<App>| async move {
let _ = app.pause().await;
"".to_owned() "".to_owned()
} }
}); });
let now_playing = warp::path!("playing").map({ let now_playing = warp::path!("playing").map({
let app = app.clone(); let state = state.clone();
move || serde_json::to_string(&app.playing()).unwrap() move || serde_json::to_string(&state.playing()).unwrap()
}); });
let routes = root let routes = root
.or(list_output_devices) .or(list_output_devices)
// .or(list_tracks) .or(list_tracks)
.or(enable_track) .or(play_track)
.or(disable_track) .or(stop_track)
.or(play_all) .or(stop_all_tracks)
.or(stop_all)
.or(pause)
.or(now_playing); .or(now_playing);
serve(routes).run(server_addr).await; serve(routes).run(server_addr).await;
} }
fn handle_add_audio_device(app: App, 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") {
app.add_audio(device_name.to_owned()); state.add_audio(device_name.to_owned());
} }
} }
} }
/* fn pipewire_loop(state: App) -> Result<(), Box<dyn std::error::Error>> {
fn pipewire_loop(app: 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)?;
@ -131,10 +95,10 @@ fn pipewire_loop(app: App) -> Result<(), Box<dyn std::error::Error>> {
let _listener = registry let _listener = registry
.add_listener_local() .add_listener_local()
.global({ .global({
let app = app.clone(); let state = state.clone();
move |global_data| { move |global_data| {
if let Some(props) = global_data.props { if let Some(props) = global_data.props {
handle_add_audio_device(app.clone(), props); handle_add_audio_device(state.clone(), props);
} }
} }
}) })
@ -144,38 +108,19 @@ fn pipewire_loop(app: App) -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
*/
/*
fn pipewire_main(state: App) { 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() {
gstreamer::init(); let state = App::default();
let (audio_control_tx, audio_control_rx) = tokio::sync::mpsc::channel(5);
let (audio_status_tx, audio_status_rx) = tokio::sync::mpsc::channel(5);
let app = Arc::new(App::new(audio_control_tx, audio_status_rx));
let audio_controller = Arc::new(AudioControl::new(GStreamerBackend::default()));
tokio::spawn({
let audio_controller = audio_controller.clone();
async move { audio_controller.listen(audio_control_rx).await }
});
tokio::spawn({
let audio_controller = audio_controller.clone();
async move { audio_controller.report(audio_status_tx).await }
});
/*
spawn_blocking({ spawn_blocking({
let app = app.clone(); let state = state.clone();
move || pipewire_main(state) move || pipewire_main(state)
}); });
*/
server_main(app.clone()).await; server_main(state.clone()).await;
} }

View File

@ -1,102 +0,0 @@
use std::{ops::Deref, path::PathBuf, time::Duration};
use thiserror::Error;
use crate::audio_control::AudioError;
#[derive(Debug, Error, PartialEq)]
pub enum AppError {
#[error("Operation invalid with no tracks enabled")]
NoTracks,
#[error("Operation is invalid in the current state")]
InvalidState,
}
impl From<AudioError> for AppError {
fn from(err: AudioError) -> Self {
match err {
AudioError::NoTracks => Self::NoTracks,
AudioError::InvalidState => Self::InvalidState,
}
}
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct TrackSpec {
pub path: PathBuf,
pub volume: Volume,
}
#[derive(Clone, Debug)]
pub struct Progress {
pub current: Duration,
pub length: Duration,
}
#[derive(Debug, Error, PartialEq)]
pub enum VolumeError {
#[error("The specified volume is out of range and must be between 0.0 and 1.0")]
OutOfRange,
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Volume(f32);
impl Volume {
pub fn as_f32(&self) -> f32 {
self.0
}
pub fn as_f64(&self) -> f64 {
self.0.into()
}
}
impl TryFrom<f32> for Volume {
type Error = VolumeError;
fn try_from(val: f32) -> Result<Self, Self::Error> {
if val < 0. || val > 1. {
return Err(VolumeError::OutOfRange);
}
Ok(Self(val))
}
}
impl From<Volume> for f32 {
fn from(val: Volume) -> f32 {
val.0
}
}
#[derive(Clone, Debug)]
pub enum AudioControlMessage {
Play,
Pause,
Stop,
EnableTrack(TrackSpec),
DisableTrack(PathBuf),
ReportStatus,
}
#[derive(Clone, Debug)]
pub struct TrackInfo {
pub path: PathBuf,
pub volume: Volume,
pub progress: Progress,
}
#[derive(Clone, Debug)]
pub struct AudioState {
pub playing: bool,
pub tracks: Vec<TrackInfo>,
}
#[derive(Debug)]
pub enum AudioStatusMessage {
Playing,
Pausing,
Status(AudioState),
AudioError(AudioError),
}