feat: first attemp to support playing multiple tracks in parallel

This commit is contained in:
2026-01-24 22:18:42 +03:00
parent c1c8deb1b3
commit 3e6a8b6e79
12 changed files with 673 additions and 351 deletions
+214 -94
View File
@@ -8,6 +8,7 @@ use crate::{
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
error::Error,
fs,
path::{Path, PathBuf},
@@ -22,19 +23,35 @@ pub enum PlayerState {
Playing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackInfo {
pub id: u32,
pub path: PathBuf,
pub duration: Option<f32>,
pub position: f32,
pub volume: f32,
pub looped: bool,
pub paused: bool,
}
pub struct PlayingSound {
pub id: u32,
pub sink: Sink,
pub path: PathBuf,
pub duration: Option<f32>,
pub looped: bool,
pub volume: f32,
}
pub struct AudioPlayer {
_stream_handle: OutputStream,
sink: Sink,
pub stream_handle: OutputStream,
pub tracks: HashMap<u32, PlayingSound>,
pub next_id: u32,
input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
pub current_input_device: Option<AudioDevice>,
pub volume: f32,
pub duration: Option<f32>,
pub current_file_path: Option<PathBuf>,
pub looped: bool,
pub volume: f32, // Master volume
}
impl AudioPlayer {
@@ -50,22 +67,16 @@ impl AudioPlayer {
}
let stream_handle = OutputStreamBuilder::open_default_stream()?;
let sink = Sink::connect_new(stream_handle.mixer());
sink.set_volume(default_volume);
let mut audio_player = AudioPlayer {
_stream_handle: stream_handle,
sink,
stream_handle,
tracks: HashMap::new(),
next_id: 1,
input_link_sender: None,
current_input_device: default_input_device.clone(),
volume: default_volume,
duration: None,
current_file_path: None,
looped: false,
};
if default_input_device.is_some() {
@@ -132,74 +143,123 @@ impl AudioPlayer {
Ok(())
}
pub fn pause(&mut self) {
if self.get_state() == PlayerState::Playing {
self.sink.pause();
}
}
pub fn resume(&mut self) {
if self.get_state() == PlayerState::Paused {
self.sink.play();
}
}
pub fn stop(&mut self) {
self.sink.stop();
}
pub fn is_paused(&self) -> bool {
self.sink.is_paused()
}
pub fn get_state(&self) -> PlayerState {
if self.sink.len() == 0 {
return PlayerState::Stopped;
}
if self.sink.is_paused() {
return PlayerState::Paused;
}
PlayerState::Playing
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume;
self.sink.set_volume(volume);
}
pub fn get_position(&self) -> f32 {
if self.get_state() == PlayerState::Stopped {
return 0.0;
}
self.sink.get_pos().as_secs_f32()
}
pub fn seek(&mut self, mut position: f32) -> Result<(), Box<dyn Error>> {
if position < 0.0 {
position = 0.0;
}
match self.sink.try_seek(Duration::from_secs_f32(position)) {
Ok(_) => Ok(()),
Err(err) => Err(err.into()),
}
}
pub fn get_duration(&mut self) -> Result<f32, Box<dyn Error>> {
if self.get_state() == PlayerState::Stopped {
Err("Nothing is playing right now".into())
pub fn pause(&mut self, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.sink.pause();
}
} else {
match self.duration {
Some(duration) => Ok(duration),
None => Err("Couldn't determine duration for current file".into()),
for sound in self.tracks.values_mut() {
sound.sink.pause();
}
}
}
pub async fn play(&mut self, file_path: &Path) -> Result<(), Box<dyn Error>> {
pub fn resume(&mut self, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.sink.play();
}
} else {
for sound in self.tracks.values_mut() {
sound.sink.play();
}
}
}
pub fn stop(&mut self, id: Option<u32>) {
if let Some(id) = id {
self.tracks.remove(&id);
} else {
self.tracks.clear();
}
}
pub fn is_paused(&self) -> bool {
if self.tracks.is_empty() {
return false;
}
self.tracks.values().all(|s| s.sink.is_paused())
}
pub fn get_state(&self) -> PlayerState {
if self.tracks.is_empty() {
return PlayerState::Stopped;
}
if self
.tracks
.values()
.any(|s| !s.sink.is_paused() && !s.sink.empty())
{
return PlayerState::Playing;
}
if self.is_paused() {
return PlayerState::Paused;
}
PlayerState::Stopped
}
pub fn set_volume(&mut self, volume: f32, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.volume = volume;
sound.sink.set_volume(self.volume * volume);
}
} else {
self.volume = volume;
for sound in self.tracks.values_mut() {
sound.sink.set_volume(self.volume * sound.volume);
}
}
}
pub fn get_position(&self, id: Option<u32>) -> f32 {
if let Some(id) = id {
if let Some(sound) = self.tracks.get(&id) {
return sound.sink.get_pos().as_secs_f32();
}
} else if let Some(sound) = self.tracks.values().last() {
// Fallback to last added track if no ID
return sound.sink.get_pos().as_secs_f32();
}
0.0
}
pub fn seek(&mut self, position: f32, id: Option<u32>) -> Result<(), Box<dyn Error>> {
let position = if position < 0.0 { 0.0 } else { position };
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.sink.try_seek(Duration::from_secs_f32(position))?;
}
} else {
// Seek all? Or last? Let's seek all for now if no ID provided
for sound in self.tracks.values_mut() {
sound.sink.try_seek(Duration::from_secs_f32(position)).ok();
}
}
Ok(())
}
pub fn get_duration(&mut self, id: Option<u32>) -> Result<f32, Box<dyn Error>> {
if let Some(id) = id {
if let Some(sound) = self.tracks.get(&id) {
return sound.duration.ok_or("Unknown duration".into());
}
} else if let Some(sound) = self.tracks.values().last() {
return sound.duration.ok_or("Unknown duration".into());
}
Err("No track playing".into())
}
pub async fn play(
&mut self,
file_path: &Path,
concurrent: bool,
) -> Result<u32, Box<dyn Error>> {
if !file_path.exists() {
return Err(format!("File does not exist: {}", file_path.display()).into());
}
@@ -207,30 +267,90 @@ impl AudioPlayer {
let file = fs::File::open(file_path)?;
match Decoder::try_from(file) {
Ok(source) => {
self.current_file_path = Some(file_path.to_path_buf());
if let Some(duration) = source.total_duration() {
self.duration = Some(duration.as_secs_f32());
} else {
self.duration = None;
if !concurrent {
self.tracks.clear();
}
self.sink.stop();
self.sink.append(source);
self.sink.play();
let id = self.next_id;
self.next_id += 1;
let duration = source.total_duration().map(|d| d.as_secs_f32());
let sink = Sink::connect_new(self.stream_handle.mixer());
sink.set_volume(self.volume); // Default volume is 1.0 * master
sink.append(source);
sink.play();
let sound = PlayingSound {
id,
sink,
path: file_path.to_path_buf(),
duration,
looped: false,
volume: 1.0,
};
self.tracks.insert(id, sound);
self.link_devices().await?;
Ok(())
Ok(id)
}
Err(err) => Err(err.into()),
}
}
pub fn get_current_file_path(&mut self) -> &Option<PathBuf> {
if self.get_state() == PlayerState::Stopped && !self.looped {
self.current_file_path = None;
pub fn set_loop(&mut self, enabled: bool, id: Option<u32>) {
if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) {
sound.looped = enabled;
}
} else {
// Set loop for all? Or just last?
// Let's set for all.
for sound in self.tracks.values_mut() {
sound.looped = enabled;
}
}
&self.current_file_path
}
pub fn get_tracks(&self) -> Vec<TrackInfo> {
self.tracks
.values()
.map(|sound| TrackInfo {
id: sound.id,
path: sound.path.clone(),
duration: sound.duration,
position: sound.sink.get_pos().as_secs_f32(),
volume: sound.volume,
looped: sound.looped,
paused: sound.sink.is_paused(),
})
.collect()
}
pub async fn update(&mut self) {
let mut restarts = vec![];
for (id, sound) in &self.tracks {
if sound.sink.empty() && sound.looped {
restarts.push(*id);
}
}
for id in restarts {
if let Some(sound) = self.tracks.get_mut(&id) {
if let Ok(file) = fs::File::open(&sound.path) {
if let Ok(source) = Decoder::try_from(file) {
sound.sink.append(source);
sound.sink.play();
}
}
}
}
self.tracks
.retain(|_, sound| !sound.sink.empty() || sound.looped);
}
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
+81 -43
View File
@@ -12,13 +12,21 @@ pub trait Executable {
pub struct PingCommand {}
pub struct PauseCommand {}
pub struct PauseCommand {
pub id: Option<u32>,
}
pub struct ResumeCommand {}
pub struct ResumeCommand {
pub id: Option<u32>,
}
pub struct TogglePauseCommand {}
pub struct TogglePauseCommand {
pub id: Option<u32>,
}
pub struct StopCommand {}
pub struct StopCommand {
pub id: Option<u32>,
}
pub struct IsPausedCommand {}
@@ -28,21 +36,28 @@ pub struct GetVolumeCommand {}
pub struct SetVolumeCommand {
pub volume: Option<f32>,
pub id: Option<u32>,
}
pub struct GetPositionCommand {}
pub struct GetPositionCommand {
pub id: Option<u32>,
}
pub struct SeekCommand {
pub position: Option<f32>,
pub id: Option<u32>,
}
pub struct GetDurationCommand {}
pub struct GetDurationCommand {
pub id: Option<u32>,
}
pub struct PlayCommand {
pub file_path: Option<PathBuf>,
pub concurrent: Option<bool>,
}
pub struct GetCurrentFilePathCommand {}
pub struct GetTracksCommand {}
pub struct GetCurrentInputCommand {}
@@ -52,13 +67,14 @@ pub struct SetCurrentInputCommand {
pub name: Option<String>,
}
pub struct GetLoopCommand {}
pub struct SetLoopCommand {
pub enabled: Option<bool>,
pub id: Option<u32>,
}
pub struct ToggleLoopCommand {}
pub struct ToggleLoopCommand {
pub id: Option<u32>,
}
#[async_trait]
impl Executable for PingCommand {
@@ -71,7 +87,7 @@ impl Executable for PingCommand {
impl Executable for PauseCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.pause();
audio_player.pause(self.id);
Response::new(true, "Audio was paused")
}
}
@@ -80,7 +96,7 @@ impl Executable for PauseCommand {
impl Executable for ResumeCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.resume();
audio_player.resume(self.id);
Response::new(true, "Audio was resumed")
}
}
@@ -94,12 +110,31 @@ impl Executable for TogglePauseCommand {
return Response::new(false, "Audio is not playing");
}
if audio_player.is_paused() {
audio_player.resume();
Response::new(true, "Audio was resumed")
// This logic is a bit tricky with multiple tracks.
// If ID is provided, toggle that track.
// If not, toggle global pause state?
// For now, let's just use pause/resume based on global state if no ID.
if let Some(id) = self.id {
if let Some(track) = audio_player.tracks.get(&id) {
if track.sink.is_paused() {
audio_player.resume(Some(id));
Response::new(true, "Audio was resumed")
} else {
audio_player.pause(Some(id));
Response::new(true, "Audio was paused")
}
} else {
Response::new(false, "Track not found")
}
} else {
audio_player.pause();
Response::new(true, "Audio was paused")
if audio_player.is_paused() {
audio_player.resume(None);
Response::new(true, "Audio was resumed")
} else {
audio_player.pause(None);
Response::new(true, "Audio was paused")
}
}
}
}
@@ -108,7 +143,7 @@ impl Executable for TogglePauseCommand {
impl Executable for StopCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.stop();
audio_player.stop(self.id);
Response::new(true, "Audio was stopped")
}
}
@@ -145,7 +180,7 @@ impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response {
if let Some(volume) = self.volume {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.set_volume(volume);
audio_player.set_volume(volume, self.id);
Response::new(true, format!("Audio volume was set to {}", volume))
} else {
Response::new(false, "Invalid volume value")
@@ -157,7 +192,7 @@ impl Executable for SetVolumeCommand {
impl Executable for GetPositionCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
let position = audio_player.get_position();
let position = audio_player.get_position(self.id);
Response::new(true, position.to_string())
}
}
@@ -167,7 +202,7 @@ impl Executable for SeekCommand {
async fn execute(&self) -> Response {
if let Some(position) = self.position {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.seek(position) {
match audio_player.seek(position, self.id) {
Ok(_) => Response::new(true, format!("Audio position was set to {}", position)),
Err(err) => Response::new(false, err.to_string()),
}
@@ -181,7 +216,7 @@ impl Executable for SeekCommand {
impl Executable for GetDurationCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.get_duration() {
match audio_player.get_duration(self.id) {
Ok(duration) => Response::new(true, duration.to_string()),
Err(err) => Response::new(false, err.to_string()),
}
@@ -193,8 +228,11 @@ impl Executable for PlayCommand {
async fn execute(&self) -> Response {
if let Some(file_path) = &self.file_path {
let mut audio_player = get_audio_player().await.lock().await;
match audio_player.play(file_path).await {
Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())),
match audio_player
.play(file_path, self.concurrent.unwrap_or(false))
.await
{
Ok(id) => Response::new(true, id.to_string()),
Err(err) => Response::new(false, err.to_string()),
}
} else {
@@ -204,15 +242,11 @@ impl Executable for PlayCommand {
}
#[async_trait]
impl Executable for GetCurrentFilePathCommand {
impl Executable for GetTracksCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
let current_file_path = audio_player.get_current_file_path();
if let Some(current_file_path) = current_file_path {
Response::new(true, current_file_path.to_str().unwrap())
} else {
Response::new(false, "No file is playing")
}
let audio_player = get_audio_player().await.lock().await;
let tracks = audio_player.get_tracks();
Response::new(true, serde_json::to_string(&tracks).unwrap())
}
}
@@ -265,14 +299,6 @@ impl Executable for SetCurrentInputCommand {
}
}
#[async_trait]
impl Executable for GetLoopCommand {
async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await;
Response::new(true, audio_player.looped.to_string())
}
}
#[async_trait]
impl Executable for SetLoopCommand {
async fn execute(&self) -> Response {
@@ -280,7 +306,7 @@ impl Executable for SetLoopCommand {
match self.enabled {
Some(enabled) => {
audio_player.looped = enabled;
audio_player.set_loop(enabled, self.id);
Response::new(true, format!("Loop was set to {}", enabled))
}
None => Response::new(false, "Invalid enabled value"),
@@ -292,7 +318,19 @@ impl Executable for SetLoopCommand {
impl Executable for ToggleLoopCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
audio_player.looped = !audio_player.looped;
Response::new(true, format!("Loop was set to {}", audio_player.looped))
if let Some(id) = self.id {
if let Some(track) = audio_player.tracks.get_mut(&id) {
track.looped = !track.looped;
Response::new(true, format!("Loop was set to {}", track.looped))
} else {
Response::new(false, "Track not found")
}
} else {
// Toggle all?
for track in audio_player.tracks.values_mut() {
track.looped = !track.looped;
}
Response::new(true, "Loop toggled for all tracks")
}
}
}
+16 -6
View File
@@ -1,21 +1,28 @@
use crate::types::audio_player::PlayerState;
use crate::types::audio_player::{PlayerState, TrackInfo};
use egui::Id;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
time::Instant,
};
#[derive(Default, Debug)]
pub struct TrackUiState {
pub position_slider_value: f32,
pub volume_slider_value: f32,
pub position_dragged: bool,
pub volume_dragged: bool,
pub ignore_position_update_until: Option<Instant>,
pub ignore_volume_update_until: Option<Instant>,
}
#[derive(Default, Debug)]
pub struct AppState {
pub search_query: String,
pub position_slider_value: f32,
pub volume_slider_value: f32,
pub position_dragged: bool,
pub volume_dragged: bool,
pub track_ui_states: HashMap<u32, TrackUiState>,
pub show_settings: bool,
@@ -33,6 +40,9 @@ pub struct AppState {
pub struct AudioPlayerState {
pub state: PlayerState,
pub new_state: Option<PlayerState>,
pub tracks: Vec<TrackInfo>,
pub current_file_path: PathBuf,
pub is_paused: bool,
+84 -28
View File
@@ -24,24 +24,54 @@ impl Request {
Request::new("ping", vec![])
}
pub fn pause() -> Self {
Request::new("pause", vec![])
pub fn pause(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("pause", args)
}
pub fn resume() -> Self {
Request::new("resume", vec![])
pub fn resume(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("resume", args)
}
pub fn toggle_pause() -> Self {
Request::new("toggle_pause", vec![])
pub fn toggle_pause(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("toggle_pause", args)
}
pub fn stop() -> Self {
Request::new("stop", vec![])
pub fn stop(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("stop", args)
}
pub fn play(file_path: &str) -> Self {
Request::new("play", vec![("file_path", file_path)])
pub fn play(file_path: &str, concurrent: bool) -> Self {
Request::new(
"play",
vec![
("file_path", file_path),
("concurrent", &concurrent.to_string()),
],
)
}
pub fn get_is_paused() -> Self {
@@ -52,20 +82,32 @@ impl Request {
Request::new("get_volume", vec![])
}
pub fn get_position() -> Self {
Request::new("get_position", vec![])
pub fn get_position(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("get_position", args)
}
pub fn get_duration() -> Self {
Request::new("get_duration", vec![])
pub fn get_duration(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("get_duration", args)
}
pub fn get_state() -> Self {
Request::new("get_state", vec![])
}
pub fn get_current_file_path() -> Self {
Request::new("get_current_file_path", vec![])
pub fn get_tracks() -> Self {
Request::new("get_tracks", vec![])
}
pub fn get_input() -> Self {
@@ -76,28 +118,42 @@ impl Request {
Request::new("get_inputs", vec![])
}
pub fn set_volume(volume: f32) -> Self {
Request::new("set_volume", vec![("volume", &volume.to_string())])
pub fn set_volume(volume: f32, id: Option<u32>) -> Self {
let mut args = vec![("volume".to_string(), volume.to_string())];
if let Some(id) = id {
args.push(("id".to_string(), id.to_string()));
}
Request::new("set_volume".to_string(), args)
}
pub fn seek(position: f32) -> Self {
Request::new("seek", vec![("position", &position.to_string())])
pub fn seek(position: f32, id: Option<u32>) -> Self {
let mut args = vec![("position".to_string(), position.to_string())];
if let Some(id) = id {
args.push(("id".to_string(), id.to_string()));
}
Request::new("seek".to_string(), args)
}
pub fn set_input(name: &str) -> Self {
Request::new("set_input", vec![("input_name", name)])
}
pub fn get_loop() -> Self {
Request::new("get_loop", vec![])
pub fn set_loop(enabled: &str, id: Option<u32>) -> Self {
let mut args = vec![("enabled".to_string(), enabled.to_string())];
if let Some(id) = id {
args.push(("id".to_string(), id.to_string()));
}
Request::new("set_loop".to_string(), args)
}
pub fn set_loop(enabled: &str) -> Self {
Request::new("set_loop", vec![("enabled", enabled)])
}
pub fn toggle_loop() -> Self {
Request::new("toggle_loop", vec![])
pub fn toggle_loop(id: Option<u32>) -> Self {
let mut args = vec![];
let id_str;
if let Some(id) = id {
id_str = id.to_string();
args.push(("id", id_str.as_str()));
}
Request::new("toggle_loop", args)
}
}