Skip to content

Commit 465405b

Browse files
authored
Merge pull request #123 from ccgauche/feat/switch-to-rodio
feat: switch to rodio
2 parents 5526d19 + 3a72676 commit 465405b

39 files changed

+648
-4059
lines changed

Cargo.lock

Lines changed: 248 additions & 281 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/player/Cargo.toml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9-
cpal = { version = "0.13.5" }
10-
symphonia = { git = "https://github.com/pdeljanov/Symphonia", features = [
11-
"aac",
12-
"isomp4",
13-
] }
149
flume = "0.11.0"
15-
tokio = "1.36.0"
16-
atomic_float = "0.1.0"
10+
rodio = { version = "0.21.1", default-features = false, features = ["playback", "symphonia-aac", "symphonia-isomp4"] }
11+
log = "0.4.29"

crates/player/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Custom Error Enum to handle different failures
2+
#[derive(Debug)]
3+
pub enum PlayError {
4+
Io(std::io::Error),
5+
DecoderError(rodio::decoder::DecoderError),
6+
StreamError(rodio::StreamError),
7+
PlayError(rodio::PlayError),
8+
SeekError(rodio::source::SeekError),
9+
}
10+
11+
impl From<rodio::PlayError> for PlayError {
12+
fn from(err: rodio::PlayError) -> Self {
13+
PlayError::PlayError(err)
14+
}
15+
}

crates/player/src/lib.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1-
mod rusty_backend;
1+
mod error;
22

3-
pub use rusty_backend::*;
3+
use std::time::Duration;
4+
5+
pub use error::PlayError;
6+
7+
mod player;
8+
pub use player::Player;
9+
10+
mod player_options;
11+
pub use player_options::PlayerOptions;
12+
13+
mod player_data;
14+
pub(crate) use player_data::PlayerData;
15+
16+
pub(crate) static VOLUME_STEP: u8 = 5;
17+
pub(crate) static SEEK_STEP: Duration = Duration::from_secs(5);

crates/player/src/player.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
use flume::Sender;
2+
use rodio::cpal::traits::HostTrait;
3+
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
4+
5+
use std::fs::File;
6+
use std::path::Path;
7+
use std::time::Duration;
8+
9+
use crate::{PlayError, PlayerData, PlayerOptions, SEEK_STEP};
10+
11+
pub struct Player {
12+
sink: Sink,
13+
stream: OutputStream,
14+
data: PlayerData,
15+
error_sender: Sender<PlayError>,
16+
options: PlayerOptions,
17+
}
18+
19+
impl Player {
20+
fn try_from_device(device: rodio::cpal::Device) -> Result<OutputStream, PlayError> {
21+
// In rodio 0.21, try_from_device is available on OutputStream
22+
OutputStreamBuilder::default()
23+
.with_device(device)
24+
.open_stream()
25+
.map_err(PlayError::StreamError)
26+
}
27+
28+
/// Try to create a stream from the default device, falling back to others
29+
fn try_default() -> Result<OutputStream, PlayError> {
30+
// Use rodio's internal cpal re-export
31+
let host = rodio::cpal::default_host();
32+
33+
let default_device = host
34+
.default_output_device()
35+
.ok_or(PlayError::StreamError(rodio::StreamError::NoDevice))?;
36+
37+
Self::try_from_device(default_device).or_else(|original_err| {
38+
let devices = host.output_devices().map_err(|_| original_err)?;
39+
40+
for d in devices {
41+
if let Ok(res) = Self::try_from_device(d) {
42+
return Ok(res);
43+
}
44+
}
45+
Err(PlayError::StreamError(rodio::StreamError::NoDevice))
46+
})
47+
}
48+
49+
pub fn new(error_sender: Sender<PlayError>, options: PlayerOptions) -> Result<Self, PlayError> {
50+
let stream = Self::try_default()?;
51+
52+
// sink::try_new requires a reference to the handle
53+
let sink = Sink::connect_new(stream.mixer());
54+
55+
sink.set_volume(options.initial_volume_f32());
56+
57+
Ok(Self {
58+
sink,
59+
stream,
60+
error_sender,
61+
data: PlayerData::new(options.initial_volume()),
62+
options,
63+
})
64+
}
65+
66+
pub fn update(&self) -> Result<Self, PlayError> {
67+
let stream = Self::try_default()?;
68+
let sink = Sink::connect_new(stream.mixer());
69+
70+
sink.set_volume(self.data.volume_f32());
71+
72+
Ok(Self {
73+
sink,
74+
stream,
75+
error_sender: self.error_sender.clone(),
76+
data: self.data.clone(),
77+
options: self.options.clone(),
78+
})
79+
}
80+
pub fn change_volume(&mut self, positive: bool) {
81+
self.data.change_volume(positive);
82+
self.sink.set_volume(self.data.volume_f32());
83+
}
84+
85+
pub fn is_finished(&self) -> bool {
86+
self.sink.empty()
87+
}
88+
89+
pub fn play_at(&mut self, path: &Path, time: Duration) -> Result<(), PlayError> {
90+
log::info!("Playing file: {:?} at time: {:?}", path, time);
91+
self.play(path)?;
92+
if let Err(e) = self.sink.try_seek(time) {
93+
log::error!("Seek error: {}", e);
94+
let _ = self.error_sender.send(PlayError::SeekError(e));
95+
}
96+
97+
Ok(())
98+
}
99+
100+
pub fn play(&mut self, path: &Path) -> Result<(), PlayError> {
101+
log::info!("Playing file: {:?}", path);
102+
self.data.set_current_file(Some(path.to_path_buf()));
103+
104+
self.stop();
105+
106+
let file = File::open(path).map_err(PlayError::Io)?;
107+
108+
if file.metadata().map(|m| m.len()).unwrap_or(0) == 0 {
109+
return Err(PlayError::Io(std::io::Error::new(
110+
std::io::ErrorKind::InvalidData,
111+
"File is empty",
112+
)));
113+
}
114+
115+
let decoder = Decoder::new(file).map_err(PlayError::DecoderError)?;
116+
117+
self.data.set_total_duration(decoder.total_duration());
118+
119+
// Check if sink is detached or empty and recreate if necessary
120+
if self.sink.empty() {
121+
// Using try_new with the stored handle
122+
self.sink = Sink::connect_new(self.stream.mixer());
123+
}
124+
125+
self.sink.set_volume(self.data.volume_f32());
126+
self.sink.append(decoder);
127+
128+
Ok(())
129+
}
130+
131+
pub fn stop(&mut self) {
132+
// rodio 0.21: To stop, you can clear the sink.
133+
if !self.sink.empty() {
134+
self.sink.clear();
135+
}
136+
}
137+
138+
pub fn elapsed(&self) -> Duration {
139+
self.sink.get_pos()
140+
}
141+
142+
pub fn duration(&self) -> Option<f64> {
143+
self.data
144+
.total_duration()
145+
.map(|duration| duration.as_secs_f64())
146+
}
147+
148+
pub fn toggle_playback(&mut self) {
149+
if self.sink.is_paused() {
150+
self.sink.play();
151+
} else {
152+
self.sink.pause();
153+
}
154+
}
155+
156+
pub fn seek_fw(&mut self) {
157+
let current_elapsed = self.elapsed();
158+
let new_pos = current_elapsed + SEEK_STEP;
159+
160+
self.seek_to(new_pos);
161+
}
162+
163+
pub fn seek_bw(&mut self) {
164+
let current_elapsed = self.elapsed();
165+
let new_pos = current_elapsed.saturating_sub(SEEK_STEP);
166+
self.seek_to(new_pos);
167+
}
168+
169+
pub fn seek_to(&mut self, time: Duration) {
170+
log::info!("Seek to: {:?}", time);
171+
if self.is_finished() {
172+
return;
173+
}
174+
let file = self.data.current_file().expect("Current file not set");
175+
176+
if let Err(e) = self.sink.try_seek(time) {
177+
log::error!("Seek error: {}", e);
178+
let _ = self.error_sender.send(PlayError::SeekError(e));
179+
} else {
180+
// If the sink is finished, we need to reset the music
181+
// This happens when the user seeks to the start of the song before the buffer.
182+
if self.is_finished() {
183+
log::info!("Sink is finished while seeking, resetting the music");
184+
if let Err(e) = self.play_at(&file, time) {
185+
log::error!("Error playing file: {:?}", e);
186+
let _ = self.error_sender.send(e);
187+
}
188+
}
189+
}
190+
}
191+
192+
pub fn percentage(&self) -> f64 {
193+
self.duration().map_or(0.0, |duration| {
194+
let elapsed = self.elapsed().as_secs_f64();
195+
elapsed / duration
196+
})
197+
}
198+
199+
pub fn volume_percent(&self) -> u8 {
200+
self.data.volume()
201+
}
202+
203+
pub fn volume(&self) -> i32 {
204+
self.data.volume().into()
205+
}
206+
207+
pub fn volume_up(&mut self) {
208+
let volume = self.volume() + 5;
209+
self.set_volume(volume);
210+
}
211+
212+
pub fn volume_down(&mut self) {
213+
let volume = self.volume() - 5;
214+
self.set_volume(volume);
215+
}
216+
217+
pub fn set_volume(&mut self, mut volume: i32) {
218+
volume = volume.clamp(0, 100);
219+
self.data.set_volume(volume as u8);
220+
self.sink.set_volume((volume as f32) / 100.0);
221+
}
222+
223+
pub fn pause(&mut self) {
224+
if !self.sink.is_paused() {
225+
self.toggle_playback();
226+
}
227+
}
228+
229+
pub fn resume(&mut self) {
230+
if self.sink.is_paused() {
231+
self.toggle_playback();
232+
}
233+
}
234+
235+
pub fn is_paused(&self) -> bool {
236+
self.sink.is_paused()
237+
}
238+
239+
pub fn seek(&mut self, secs: i64) {
240+
if secs.is_positive() {
241+
self.seek_fw();
242+
} else {
243+
self.seek_bw();
244+
}
245+
}
246+
247+
pub fn get_progress(&self) -> (f64, u32, u32) {
248+
let position = self.elapsed();
249+
let duration = self.duration().unwrap_or(99.0) as u32;
250+
let percent = self.percentage() * 100.0;
251+
(percent.min(100.0), position.as_secs() as u32, duration)
252+
}
253+
}

crates/player/src/player_data.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::{path::PathBuf, time::Duration};
2+
3+
use crate::VOLUME_STEP;
4+
5+
#[derive(Clone)]
6+
pub struct PlayerData {
7+
total_duration: Option<Duration>,
8+
current_file: Option<PathBuf>,
9+
volume: u8,
10+
}
11+
12+
impl PlayerData {
13+
pub fn new(volume: u8) -> Self {
14+
Self {
15+
total_duration: None,
16+
current_file: None,
17+
volume,
18+
}
19+
}
20+
21+
/// Changes the volume by the volume step. If positive is true, the volume is increased, otherwise it is decreased.
22+
pub fn change_volume(&mut self, positive: bool) {
23+
if positive {
24+
self.set_volume(self.volume().saturating_add(VOLUME_STEP).min(100));
25+
} else {
26+
self.set_volume(self.volume().saturating_sub(VOLUME_STEP));
27+
}
28+
}
29+
30+
/// Returns the volume as a f32 between 0.0 and 1.0
31+
pub fn volume_f32(&self) -> f32 {
32+
f32::from(self.volume()) / 100.0
33+
}
34+
35+
/// Returns the volume as a u8 between 0 and 100
36+
pub fn volume(&self) -> u8 {
37+
self.volume
38+
}
39+
40+
/// Sets the volume to the given value
41+
pub fn set_volume(&mut self, volume: u8) {
42+
self.volume = volume;
43+
}
44+
45+
/// Returns the total duration of the current file
46+
pub fn total_duration(&self) -> Option<Duration> {
47+
self.total_duration
48+
}
49+
50+
/// Sets the total duration of the current file
51+
pub fn set_total_duration(&mut self, total_duration: Option<Duration>) {
52+
self.total_duration = total_duration;
53+
}
54+
55+
/// Returns the current file
56+
pub fn current_file(&self) -> Option<PathBuf> {
57+
self.current_file.clone()
58+
}
59+
60+
/// Sets the current file
61+
pub fn set_current_file(&mut self, current_file: Option<PathBuf>) {
62+
self.current_file = current_file;
63+
}
64+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#[derive(Debug, Clone)]
2+
pub struct PlayerOptions {
3+
initial_volume: u8,
4+
}
5+
6+
impl PlayerOptions {
7+
/// Creates a new PlayerOptions with the given initial volume
8+
pub fn new(initial_volume: u8) -> Self {
9+
Self {
10+
initial_volume: initial_volume.min(100),
11+
}
12+
}
13+
14+
/// Returns the initial volume as a u8 between 0 and 100
15+
pub fn initial_volume(&self) -> u8 {
16+
self.initial_volume
17+
}
18+
19+
/// Returns the initial volume as a f32 between 0.0 and 1.0
20+
pub fn initial_volume_f32(&self) -> f32 {
21+
f32::from(self.initial_volume()) / 100.0
22+
}
23+
}

0 commit comments

Comments
 (0)