Compare commits
16 Commits
main
...
gm-dash__l
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | f4cab900a1 | |
Savanni D'Gerinel | 4eee5321d2 | |
Savanni D'Gerinel | 5acc84e423 | |
Savanni D'Gerinel | 118428d545 | |
Savanni D'Gerinel | 7da7ffcaa5 | |
Savanni D'Gerinel | cbe1a90fcb | |
Savanni D'Gerinel | f555804f10 | |
Savanni D'Gerinel | f941d1fb66 | |
Savanni D'Gerinel | 36d489e8a2 | |
Savanni D'Gerinel | 281bef855b | |
Savanni D'Gerinel | 7467e8d5b2 | |
Savanni D'Gerinel | 6b245ac9a0 | |
Savanni D'Gerinel | 426d42eb71 | |
Savanni D'Gerinel | 04a6e607a3 | |
Savanni D'Gerinel | ee56513299 | |
Savanni D'Gerinel | f8fdaf2892 |
File diff suppressed because it is too large
Load Diff
|
@ -17,7 +17,7 @@ use rp_pico::{
|
||||||
entry,
|
entry,
|
||||||
hal::{
|
hal::{
|
||||||
clocks::init_clocks_and_plls,
|
clocks::init_clocks_and_plls,
|
||||||
gpio::{FunctionSio, Pin, PinId, PullUp, SioInput},
|
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
|
||||||
pac::{CorePeripherals, Peripherals},
|
pac::{CorePeripherals, Peripherals},
|
||||||
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||||
watchdog::Watchdog,
|
watchdog::Watchdog,
|
||||||
|
@ -65,12 +65,16 @@ struct BikeUI<
|
||||||
RightId: PinId,
|
RightId: PinId,
|
||||||
PreviousId: PinId,
|
PreviousId: PinId,
|
||||||
NextId: PinId,
|
NextId: PinId,
|
||||||
|
BrakeId: PinId,
|
||||||
> {
|
> {
|
||||||
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||||
left_blinker_button: DebouncedButton<LeftId>,
|
left_blinker_button: DebouncedButton<LeftId>,
|
||||||
right_blinker_button: DebouncedButton<RightId>,
|
right_blinker_button: DebouncedButton<RightId>,
|
||||||
previous_animation_button: DebouncedButton<PreviousId>,
|
previous_animation_button: DebouncedButton<PreviousId>,
|
||||||
next_animation_button: DebouncedButton<NextId>,
|
next_animation_button: DebouncedButton<NextId>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
|
||||||
|
brake_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
|
@ -80,7 +84,8 @@ impl<
|
||||||
RightId: PinId,
|
RightId: PinId,
|
||||||
PreviousId: PinId,
|
PreviousId: PinId,
|
||||||
NextId: PinId,
|
NextId: PinId,
|
||||||
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId>
|
BrakeId: PinId,
|
||||||
|
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
{
|
{
|
||||||
fn new(
|
fn new(
|
||||||
spi: Spi<Enabled, D, P, 8>,
|
spi: Spi<Enabled, D, P, 8>,
|
||||||
|
@ -88,6 +93,7 @@ impl<
|
||||||
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
|
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
spi: RefCell::new(spi),
|
spi: RefCell::new(spi),
|
||||||
|
@ -95,6 +101,9 @@ impl<
|
||||||
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
right_blinker_button: DebouncedButton::new(right_blinker_button),
|
||||||
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
previous_animation_button: DebouncedButton::new(previous_animation_button),
|
||||||
next_animation_button: DebouncedButton::new(next_animation_button),
|
next_animation_button: DebouncedButton::new(next_animation_button),
|
||||||
|
brake_sensor,
|
||||||
|
|
||||||
|
brake_enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,10 +115,17 @@ impl<
|
||||||
RightId: PinId,
|
RightId: PinId,
|
||||||
PreviousId: PinId,
|
PreviousId: PinId,
|
||||||
NextId: PinId,
|
NextId: PinId,
|
||||||
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId>
|
BrakeId: PinId,
|
||||||
|
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
|
||||||
{
|
{
|
||||||
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
|
||||||
if self.left_blinker_button.is_low(current_time) {
|
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
|
||||||
|
self.brake_enabled = true;
|
||||||
|
Some(Event::Brake)
|
||||||
|
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
|
||||||
|
self.brake_enabled = false;
|
||||||
|
Some(Event::BrakeRelease)
|
||||||
|
} else if self.left_blinker_button.is_low(current_time) {
|
||||||
self.left_blinker_button.set_debounce(current_time);
|
self.left_blinker_button.set_debounce(current_time);
|
||||||
Some(Event::LeftBlinker)
|
Some(Event::LeftBlinker)
|
||||||
} else if self.right_blinker_button.is_low(current_time) {
|
} else if self.right_blinker_button.is_low(current_time) {
|
||||||
|
@ -193,10 +209,13 @@ fn main() -> ! {
|
||||||
embedded_hal::spi::MODE_1,
|
embedded_hal::spi::MODE_1,
|
||||||
);
|
);
|
||||||
|
|
||||||
let left_blinker_button = pins.gpio17.into_pull_up_input();
|
let left_blinker_button = pins.gpio16.into_pull_up_input();
|
||||||
let right_blinker_button = pins.gpio16.into_pull_up_input();
|
let right_blinker_button = pins.gpio17.into_pull_up_input();
|
||||||
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
let previous_animation_button = pins.gpio27.into_pull_up_input();
|
||||||
let next_animation_button = pins.gpio26.into_pull_up_input();
|
let next_animation_button = pins.gpio26.into_pull_up_input();
|
||||||
|
let brake_sensor = pins.gpio18.into_pull_up_input();
|
||||||
|
|
||||||
|
let mut led_pin = pins.led.into_push_pull_output();
|
||||||
|
|
||||||
let ui = BikeUI::new(
|
let ui = BikeUI::new(
|
||||||
spi,
|
spi,
|
||||||
|
@ -204,11 +223,11 @@ fn main() -> ! {
|
||||||
right_blinker_button,
|
right_blinker_button,
|
||||||
previous_animation_button,
|
previous_animation_button,
|
||||||
next_animation_button,
|
next_animation_button,
|
||||||
|
brake_sensor,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut app = App::new(Box::new(ui));
|
let mut app = App::new(Box::new(ui));
|
||||||
|
|
||||||
let mut led_pin = pins.led.into_push_pull_output();
|
|
||||||
led_pin.set_high();
|
led_pin.set_high();
|
||||||
|
|
||||||
let mut time = Instant::default();
|
let mut time = Instant::default();
|
||||||
|
|
|
@ -20,16 +20,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1704732714,
|
"lastModified": 1724316499,
|
||||||
"narHash": "sha256-ABqK/HggMYA/jMUXgYyqVAcQ8QjeMyr1jcXfTpSHmps=",
|
"narHash": "sha256-Qb9MhKBUTCfWg/wqqaxt89Xfi6qTD3XpTzQ9eXi3JmE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6723fa4e4f1a30d42a633bef5eb01caeb281adc3",
|
"rev": "797f7dc49e0bc7fab4b57c021cdf68f595e47841",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixos-23.11",
|
"ref": "nixos-24.05",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
description = "Lumenescent Dreams Tools";
|
description = "Lumenescent Dreams Tools";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "nixpkgs/nixos-23.11";
|
nixpkgs.url = "nixpkgs/nixos-24.05";
|
||||||
unstable.url = "nixpkgs/nixos-unstable";
|
unstable.url = "nixpkgs/nixos-unstable";
|
||||||
typeshare.url = "github:1Password/typeshare";
|
typeshare.url = "github:1Password/typeshare";
|
||||||
};
|
};
|
||||||
|
@ -30,6 +30,9 @@
|
||||||
pkgs.gst_all_1.gst-plugins-good
|
pkgs.gst_all_1.gst-plugins-good
|
||||||
pkgs.gst_all_1.gst-plugins-ugly
|
pkgs.gst_all_1.gst-plugins-ugly
|
||||||
pkgs.gst_all_1.gstreamer
|
pkgs.gst_all_1.gstreamer
|
||||||
|
pkgs.gst_all_1.gstreamer.dev
|
||||||
|
pkgs.gst_all_1.gst-libav
|
||||||
|
pkgs.gst_all_1.gst-vaapi
|
||||||
pkgs.gtk4
|
pkgs.gtk4
|
||||||
pkgs.libadwaita
|
pkgs.libadwaita
|
||||||
pkgs.librsvg
|
pkgs.librsvg
|
||||||
|
|
|
@ -6,8 +6,15 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pipewire = "0.8.0"
|
gstreamer = { version = "0.23.0", features = ["serde", "v1_24"] }
|
||||||
|
pipewire = { version = "0.8.0" }
|
||||||
serde = { version = "1.0.209", features = ["alloc", "derive"] }
|
serde = { version = "1.0.209", features = ["alloc", "derive"] }
|
||||||
serde_json = "1.0.127"
|
serde_json = { version = "1.0.127" }
|
||||||
tokio = { version = "1.39.3", features = ["full"] }
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
warp = "0.3.7"
|
warp = { version = "0.3.7" }
|
||||||
|
glib = { version = "0.18" }
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
cool_asserts = "2.0.3"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
reqwest = { version = "0.12.7", features = ["json"] }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
bridge_address: "192.168.1.159"
|
||||||
|
hue_api_key: "lw77sNOqZNt2YMxmXb63qEvGCgiokMyrJHfBOL1X"
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap, fs::File, path::PathBuf, sync::{Arc, RwLock}
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
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![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct Configuration {
|
||||||
|
pub bridge_address: String,
|
||||||
|
pub hue_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
fn load_from_file(path: PathBuf) -> Self {
|
||||||
|
let f = File::open(path).unwrap();
|
||||||
|
serde_yaml::from_reader(&f).expect("yaml to unwrap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
state: Arc<RwLock<AppState>>,
|
||||||
|
|
||||||
|
audio_control: Sender<AudioControlMessage>,
|
||||||
|
|
||||||
|
listener: JoinHandle<()>,
|
||||||
|
|
||||||
|
pub config: Configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(
|
||||||
|
audio_control: Sender<AudioControlMessage>,
|
||||||
|
mut audio_status: Receiver<AudioStatusMessage>,
|
||||||
|
) -> App {
|
||||||
|
let state = Arc::new(RwLock::new(AppState::default()));
|
||||||
|
let config = Configuration::load_from_file(PathBuf::from("./config.yaml"));
|
||||||
|
|
||||||
|
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,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
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!()
|
||||||
|
}
|
|
@ -0,0 +1,398 @@
|
||||||
|
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 = pipeline.clone();
|
||||||
|
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(
|
||||||
|
gstreamer::ClockTime::from_mseconds(100),
|
||||||
|
&[
|
||||||
|
MessageType::Error,
|
||||||
|
MessageType::Eos,
|
||||||
|
MessageType::StateChanged,
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
*/
|
||||||
|
if let Some(msg) = bus.timed_pop(gstreamer::ClockTime::from_mseconds(100)) {
|
||||||
|
match msg.view() {
|
||||||
|
MessageView::StateChanged(st) => {
|
||||||
|
println!("state changed: {:?}", 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");
|
||||||
|
}
|
||||||
|
msg => {
|
||||||
|
println!("{:?}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.read().unwrap().playing {
|
||||||
|
let mut q = gstreamer::query::Position::new(gstreamer::Format::Time);
|
||||||
|
pipeline.query(&mut q);
|
||||||
|
println!("Position: {:?}", q.result());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if st.playing {
|
||||||
|
self.pipeline.set_state(gstreamer::State::Paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
let volume = volume.clone();
|
||||||
|
move |_, pad| {
|
||||||
|
let next_pad = volume.static_pad("sink").unwrap();
|
||||||
|
pad.link(&next_pad).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if st.playing {
|
||||||
|
source.set_state(gstreamer::State::Paused).unwrap();
|
||||||
|
decoder.set_state(gstreamer::State::Paused).unwrap();
|
||||||
|
volume.set_state(gstreamer::State::Paused).unwrap();
|
||||||
|
self.pipeline.set_state(gstreamer::State::Playing).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 */
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.global(|global| {
|
.global(|global| {
|
||||||
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
|
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
|
||||||
|
// println!("{:?}", global.props.map(|p| p));
|
||||||
println!(
|
println!(
|
||||||
"\t{:?} {:?}",
|
"\t{:?} {:?} {:?}",
|
||||||
|
global.props.and_then(|p| p.get("node.name")),
|
||||||
global.props.and_then(|p| p.get("node.description")),
|
global.props.and_then(|p| p.get("node.description")),
|
||||||
global.props.and_then(|p| p.get("media.class"))
|
global.props.and_then(|p| p.get("media.class"))
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gstreamer::{
|
||||||
|
prelude::*, Bus, Element, EventType, MessageType, MessageView, Pad, PadDirection, PadPresence,
|
||||||
|
PadProbeData, PadProbeInfo, PadProbeReturn, PadTemplate, Pipeline,
|
||||||
|
};
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
gstreamer::init();
|
||||||
|
|
||||||
|
let pipeline = gstreamer::Pipeline::new();
|
||||||
|
let pipeline_object = pipeline.clone().upcast::<gstreamer::Object>();
|
||||||
|
|
||||||
|
let sinkfactory = gstreamer::ElementFactory::find("pulsesink")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap();
|
||||||
|
let audio_template = sinkfactory
|
||||||
|
.static_pad_templates()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.map(|template| template.get())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let audio_output = sinkfactory.create().name("sink").build().unwrap();
|
||||||
|
pipeline.add(&audio_output).unwrap();
|
||||||
|
|
||||||
|
let funnel = gstreamer::ElementFactory::find("audiomixer")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap()
|
||||||
|
.create()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
pipeline.add(&funnel).unwrap();
|
||||||
|
|
||||||
|
let convert = gstreamer::ElementFactory::find("audioconvert")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap()
|
||||||
|
.create()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
pipeline.add(&convert).unwrap();
|
||||||
|
funnel.link(&convert).unwrap();
|
||||||
|
convert.link(&audio_output).unwrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
setup_file_reader(
|
||||||
|
&pipeline,
|
||||||
|
funnel.clone(),
|
||||||
|
"/home/savanni/Music/technical-station.ogg",
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
setup_file_reader(&pipeline, funnel.clone(), "/home/savanni/Music/techno-city-day.ogg");
|
||||||
|
|
||||||
|
let bus = pipeline.bus().unwrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
let btsink = sinkfactory
|
||||||
|
.create()
|
||||||
|
.name("sink")
|
||||||
|
.property("device", "bluez_output.0C_A6_94_75_6E_8F.1")
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
*/
|
||||||
|
|
||||||
|
pipeline.set_state(gstreamer::State::Playing).unwrap();
|
||||||
|
let pipeline_object = pipeline.clone().upcast::<gstreamer::Object>();
|
||||||
|
|
||||||
|
/*
|
||||||
|
std::thread::spawn({
|
||||||
|
let bus = bus.clone();
|
||||||
|
let pipeline = pipeline.clone();
|
||||||
|
move || {
|
||||||
|
std::thread::sleep(Duration::from_secs(5));
|
||||||
|
swap_audio_output(bus, pipeline, resample, defaultsink, btsink);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
pipeline.set_state(gstreamer::State::Playing).unwrap();
|
||||||
|
|
||||||
|
let mut playing = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(msg) = bus.timed_pop_filtered(
|
||||||
|
gstreamer::ClockTime::from_mseconds(100),
|
||||||
|
&[
|
||||||
|
MessageType::Error,
|
||||||
|
MessageType::Eos,
|
||||||
|
MessageType::Progress,
|
||||||
|
MessageType::StateChanged,
|
||||||
|
MessageType::StructureChange,
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
match msg.view() {
|
||||||
|
MessageView::Progress(prog) => {
|
||||||
|
println!("progress: {:?}", prog);
|
||||||
|
}
|
||||||
|
MessageView::StateChanged(st) => {
|
||||||
|
if msg.src() == Some(&pipeline_object) {
|
||||||
|
println!("State changed from {:?} to {:?}", st.old(), st.current());
|
||||||
|
playing = st.current() == gstreamer::State::Playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessageView::StructureChange(change) => {
|
||||||
|
println!("structure change: {:?}", change);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("{:?}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if playing {
|
||||||
|
let mut q = gstreamer::query::Position::new(gstreamer::Format::Time);
|
||||||
|
pipeline.query(&mut q);
|
||||||
|
println!("Position result: {:?}", q.result());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.set_state(gstreamer::State::Null).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_pad_added(element: &Element, pad: &Pad, next_element: &Element, template: &PadTemplate) {
|
||||||
|
println!("handle_pad_added");
|
||||||
|
println!("\t{:?}", element);
|
||||||
|
println!("\t{:?}, {:?}", pad, pad.current_caps());
|
||||||
|
/*
|
||||||
|
let audio_caps = gstreamer::caps::Caps::builder()
|
||||||
|
.field("audio", "audio/x-raw,
|
||||||
|
.build();
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
let audio_pad_template = PadTemplate::new(
|
||||||
|
"audio-pad-template",
|
||||||
|
PadDirection::Sink,
|
||||||
|
PadPresence::Request,
|
||||||
|
&pad.current_caps().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
*/
|
||||||
|
let next_pad = next_element.request_pad(template, None, None).unwrap();
|
||||||
|
// let converter_pad = converter.static_pad("sink").unwrap();
|
||||||
|
pad.link(&next_pad).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_file_reader(pipeline: &Pipeline, dest: Element, path: &str) {
|
||||||
|
let source = gstreamer::ElementFactory::find("filesrc")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap()
|
||||||
|
.create()
|
||||||
|
.property("location", path)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let decoder = gstreamer::ElementFactory::find("decodebin")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap()
|
||||||
|
.create()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let volume = gstreamer::ElementFactory::find("volume")
|
||||||
|
.unwrap()
|
||||||
|
.load()
|
||||||
|
.unwrap()
|
||||||
|
.create()
|
||||||
|
.property("mute", false)
|
||||||
|
.property("volume", 0.5)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
pipeline.add(&source).unwrap();
|
||||||
|
pipeline.add(&decoder).unwrap();
|
||||||
|
pipeline.add(&volume).unwrap();
|
||||||
|
|
||||||
|
source.link(&decoder).unwrap();
|
||||||
|
|
||||||
|
let next_pad = dest.request_pad_simple("sink_%u").unwrap();
|
||||||
|
let volume_output = volume.static_pad("src").unwrap();
|
||||||
|
volume_output.link(&next_pad).unwrap();
|
||||||
|
|
||||||
|
decoder.connect_pad_added(
|
||||||
|
move |element, pad| handle_decoder_started(element, pad, volume.clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_decoder_started(_: &Element, pad: &Pad, next: Element) {
|
||||||
|
println!("connecting file decoder to converter stream");
|
||||||
|
// let next_pad = next.request_pad_simple("sink_%u").unwrap();
|
||||||
|
let next_pad = next.static_pad("sink").unwrap();
|
||||||
|
pad.link(&next_pad).unwrap();
|
||||||
|
}
|
|
@ -1,109 +1,200 @@
|
||||||
use pipewire::{context::Context, main_loop::MainLoop};
|
|
||||||
use std::{
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
net::{Ipv6Addr, SocketAddrV6},
|
net::{Ipv6Addr, SocketAddrV6},
|
||||||
sync::{Arc, RwLock},
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
use audio_control::{AudioControl, GStreamerBackend};
|
||||||
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::Deserialize;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use warp::{serve, Filter};
|
use warp::{reject::Rejection, reply::Reply, serve, Filter};
|
||||||
|
|
||||||
struct State_ {
|
mod app;
|
||||||
device_list: Vec<String>,
|
mod audio_control;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PlayTrackParams {
|
||||||
|
track_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
fn with_app(app: Arc<App>) -> impl Filter<Extract = (Arc<App>,), Error = Infallible> + Clone {
|
||||||
struct State {
|
warp::any().map(move || app.clone())
|
||||||
internal: Arc<RwLock<State_>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
fn sound_routes(app: Arc<App>) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
|
||||||
fn new() -> State {
|
let list_output_devices = warp::path!("api" / "v1" / "output_devices").map({
|
||||||
let internal = State_ {
|
let app = app.clone();
|
||||||
device_list: vec![],
|
|
||||||
};
|
|
||||||
State {
|
|
||||||
internal: Arc::new(RwLock::new(internal)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_audio(&self, device: String) {
|
|
||||||
let mut st = self.internal.write().unwrap();
|
|
||||||
(*st).device_list.push(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_devices(&self) -> Vec<String> {
|
|
||||||
let st = self.internal.read().unwrap();
|
|
||||||
(*st).device_list.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for State {
|
|
||||||
fn default() -> State {
|
|
||||||
State::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn server_main(state: State) {
|
|
||||||
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
|
||||||
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
|
||||||
|
|
||||||
let root = warp::path!().map(|| "ok".to_string());
|
|
||||||
let list_output_devices = warp::path!("output_devices").map({
|
|
||||||
let state = state.clone();
|
|
||||||
move || {
|
move || {
|
||||||
let devices = state.audio_devices();
|
let devices = app.audio_devices();
|
||||||
serde_json::to_string(&devices).unwrap()
|
serde_json::to_string(&devices).unwrap()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let routes = root.or(list_output_devices);
|
/*
|
||||||
|
let list_tracks = warp::path!("tracks").map({
|
||||||
|
let app = app.clone();
|
||||||
|
move || serde_json::to_string(&app.tracks()).unwrap()
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
let enable_track = warp::put()
|
||||||
|
.and(warp::path!("api" / "v1" / "playing"))
|
||||||
|
.and(warp::body::json())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then(|params: PlayTrackParams, app: Arc<App>| async move {
|
||||||
|
println!("enable track");
|
||||||
|
let _ = app.enable_track(PathBuf::from(params.track_name)).await;
|
||||||
|
"".to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
let disable_track = warp::delete()
|
||||||
|
.and(warp::path!("api" / "v1" / "playing"))
|
||||||
|
.and(warp::body::json())
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then(|params: PlayTrackParams, app: Arc<App>| async move {
|
||||||
|
let _ = app.disable_track(¶ms.track_name);
|
||||||
|
"".to_owned()
|
||||||
|
});
|
||||||
|
|
||||||
|
let play_all = warp::post()
|
||||||
|
.and(warp::path!("api" / "v1" / "play"))
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then({
|
||||||
|
|app: Arc<App>| async move {
|
||||||
|
println!("play_all");
|
||||||
|
let _ = app.play().await;
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stop_all = warp::post()
|
||||||
|
.and(warp::path!("api" / "v1" / "stop"))
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then({
|
||||||
|
|app: Arc<App>| async move {
|
||||||
|
let _ = app.stop().await;
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let pause = warp::post()
|
||||||
|
.and(warp::path!("api" / "v1" / "pause"))
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then({
|
||||||
|
|app: Arc<App>| async move {
|
||||||
|
let _ = app.pause().await;
|
||||||
|
"".to_owned()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let now_playing = warp::path!("api" / "v1" / "playing").map({
|
||||||
|
let app = app.clone();
|
||||||
|
move || serde_json::to_string(&app.playing()).unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
list_output_devices
|
||||||
|
// .or(list_tracks)
|
||||||
|
.or(enable_track)
|
||||||
|
.or(disable_track)
|
||||||
|
.or(play_all)
|
||||||
|
.or(stop_all)
|
||||||
|
.or(pause)
|
||||||
|
.or(now_playing)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
struct RoomMetadata {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
struct Room {
|
||||||
|
id: String,
|
||||||
|
metadata: RoomMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn light_routes(
|
||||||
|
app: Arc<App>,
|
||||||
|
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
|
||||||
|
/*
|
||||||
|
let list_lights = warp::path!("api" / "v1" / "lights").map({
|
||||||
|
let app = app.clone();
|
||||||
|
move || {
|
||||||
|
let devices = app.audio_devices();
|
||||||
|
serde_json::to_string(&devices).unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
let list_rooms = warp::path!("api" / "v1" / "rooms")
|
||||||
|
.and(with_app(app.clone()))
|
||||||
|
.then({
|
||||||
|
|app: Arc<App>| async move {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.request(Method::GET, "https://192.168.1.159/clip/v2/resource/room")
|
||||||
|
.header("hue-application-key", app.config.hue_api_key.clone())
|
||||||
|
.danger_disable_hostname_verification()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let rooms: Vec<Room> = client.execute(request).await.unwrap().json().await.unwrap();
|
||||||
|
let room_names = rooms
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| r.metadata.name)
|
||||||
|
.collect::<String>();
|
||||||
|
serde_json::to_string(&room_names).unwrap()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
list_rooms
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_main(app: Arc<App>) {
|
||||||
|
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
||||||
|
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
||||||
|
|
||||||
|
let root = warp::path!().map(|| "ok".to_string());
|
||||||
|
|
||||||
|
let routes = root
|
||||||
|
.or(sound_routes(app.clone()))
|
||||||
|
.or(light_routes(app.clone()));
|
||||||
|
|
||||||
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(app: 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());
|
app.add_audio(device_name.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mainloop = MainLoop::new(None)?;
|
|
||||||
let context = Context::new(&mainloop)?;
|
|
||||||
let core = context.connect(None)?;
|
|
||||||
let registry = core.get_registry()?;
|
|
||||||
|
|
||||||
let _listener = registry
|
|
||||||
.add_listener_local()
|
|
||||||
.global({
|
|
||||||
let state = state.clone();
|
|
||||||
move |global_data| {
|
|
||||||
if let Some(props) = global_data.props {
|
|
||||||
handle_add_audio_device(state.clone(), props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.register();
|
|
||||||
|
|
||||||
mainloop.run();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pipewire_main(state: State) {
|
|
||||||
pipewire_loop(state).expect("pipewire should not error");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let state = State::default();
|
let _ = gstreamer::init();
|
||||||
|
|
||||||
spawn_blocking({
|
let (audio_control_tx, audio_control_rx) = tokio::sync::mpsc::channel(5);
|
||||||
let state = state.clone();
|
let (audio_status_tx, audio_status_rx) = tokio::sync::mpsc::channel(5);
|
||||||
move || pipewire_main(state)
|
|
||||||
|
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 }
|
||||||
});
|
});
|
||||||
|
|
||||||
server_main(state.clone()).await;
|
server_main(app.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
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),
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.77.0"
|
channel = "1.80.1"
|
||||||
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]
|
||||||
|
|
Loading…
Reference in New Issue