rodio
rodio copied to clipboard
Playing Mp3 only at cost of blocking UI thread
Help wanted on my first rust project that goes beyond cli todo / hangman.
I am trying to get a Audio Visualizer App going (FFT wave), but am a bit stuck on the start of my little learning project.
I cant make my mp3 play in the background without freezing the main thread. Here is my code:
use iced::{
widget::{Button, Column, Text},
Application, Command,
};
use native_dialog::FileDialog;
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
use std::fs::File;
use std::path::PathBuf;
pub struct AudioVisualizer {
file_path: Option<PathBuf>,
audio_output: Option<OutputStreamHandle>,
audio_sink: Option<Sink>,
}
#[derive(Debug, Clone)]
pub enum Message {
OpenPressed,
PlayPressed,
}
impl Application for AudioVisualizer {
type Executor = iced::executor::Default;
type Message = Message;
type Flags = ();
type Theme = iced::Theme;
fn new(_flags: Self::Flags) -> (AudioVisualizer, Command<Self::Message>) {
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
(
AudioVisualizer {
file_path: None,
audio_output: Some(stream_handle),
audio_sink: None,
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Audio Visualizer")
}
fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
match _message {
Message::OpenPressed => {
match FileDialog::new()
.add_filter("Audio Files", &["mp3"])
.show_open_single_file()
{
Ok(Some(path)) => {
println!("File selected: {:?}", path.file_name());
self.file_path = Some(path);
}
Ok(None) => {
self.file_path = None;
}
Err(err) => {
println!("File dialog error: {:?}", err);
self.file_path = None;
}
}
}
Message::PlayPressed => {
println!("Start pressed");
if let Some(path) = &self.file_path {
match File::open(path) {
Ok(file) => match Decoder::new(std::io::BufReader::new(file)) {
Ok(source) => {
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
match Sink::try_new(&stream_handle) {
Ok(sink) => {
sink.append(source);
// FIXME keep ref to sink and stream_handle and let rodio stream in the background without blocking UI thread
// self.audio_sink = Some(sink);
// self.audio_output = Some(stream_handle);
sink.sleep_until_end()
}
Err(e) => println!("Error creating sink: {:?}", e),
}
}
Err(e) => println!("Error decoding audio: {:?}", e),
},
Err(e) => println!("Error opening file: {:?}", e),
}
}
}
}
Command::none()
}
fn view(&self) -> iced::Element<'_, Self::Message> {
let file_name = self
.file_path
.as_ref()
.and_then(|path| path.file_name())
.and_then(|os_str| os_str.to_str())
.map(|s| s.to_string())
.unwrap_or("-".to_string());
let open_button = Button::new(Text::new("Open")).on_press(Message::OpenPressed);
let file_text = Text::new(file_name);
let play_button = Button::new(Text::new("Play")).on_press(Message::PlayPressed);
Column::new()
.push(open_button)
.push(file_text)
.push(play_button)
.into()
}
fn theme(&self) -> Self::Theme {
Self::Theme::Dark
}
}
https://github.com/EliasDerHai/AudioVisualizer
This is due to rodio::sink::Sink::sleep_until_end() ofc, but for me that was the only way to get the mp3 playing at all. According to this post on r/rust_gamedev it should work without spawning an extra thread.
I use: iced = "0.5.2" native-dialog = "0.7.0" rodio = "0.17.3"
(OS Windows 10)
Could someone point me towards my mistake? Is the reddit post correct that the mp3 should play async without further doings or should I dedicate a new thread for this?
I think your problem is that you drop the stream and/or that you do not detach the sink.
I would recommend taking another look at the docs and the examples. Try making a minimal example without all the UI stuff around it.
Once you get it working let us know how we could improve the docs to make it easier.
If you still can't figure it out, the rust user forum is the best place to ask questions. Issues like these are more meant for addressing bugs or missing features.
Good luck!
I have the same issue, I try to get rodio running with iced UI together. However, I have a small test app that does not use any UI, but a struct. It seems that the sink dies as soon as it is moved somewhere. The code below does not play anything. When I put the rodio code in main, everything works as expected.
struct Player {
sink: Sink,
}
impl Player {
fn new() -> Self {
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
let sink = Sink::try_new(&handle).unwrap();
Self {
sink,
}
}
fn play(&self, file: &str) {
println!("Playing: {}", file);
let file = std::fs::File::open(file).unwrap();
let source = rodio::Decoder::new(BufReader::new(file)).unwrap();
self.sink.append(source);
self.sink.play();
}
}
pub fn main() {
let player = Player::new();
player.play("music/music.mp3");
thread::sleep(Duration::from_millis(3000));
}
I have the same issue, I try to get rodio running with iced UI together. However, I have a small test app that does not use any UI, but a struct. It seems that the sink dies as soon as it is moved somewhere. The code below does not play anything. When I put the rodio code in main, everything works as expected.
fn new() -> Self { let (_stream, handle) = >rodio::OutputStream::try_default().unwrap(); let sink = Sink::try_new(&handle).unwrap(); Self { sink, } }
At the end of the new function you drop stream, the sink
needs the stream though, thus it does not work anymore. That is why OutputStream::try_default
returns the stream.
To solve your issue store the stream in Player too.
Ah yes, I missed that the stream is relevant, because it is not used in the examples. I didn't think of that it has to live as long as the sink. Thanks a lot! Now it works with iced as well.
Ah yes, I missed that the stream is relevant, because it is not used in the examples.
It could be better documented, right now its only mentioned in the docs of OutputStream
.
If you want to you could add a comment to an example and mention it in some more places in the docs (maybe even in bold). I think documentation PR's to rodio are always welcome!