rodio icon indicating copy to clipboard operation
rodio copied to clipboard

Playing Mp3 only at cost of blocking UI thread

Open EliasDerHai opened this issue 1 year ago • 5 comments

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?

EliasDerHai avatar Dec 05 '23 18:12 EliasDerHai

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!

dvdsk avatar Dec 10 '23 20:12 dvdsk

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));
}

kayhannay avatar Dec 16 '23 14:12 kayhannay

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.

dvdsk avatar Dec 16 '23 15:12 dvdsk

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.

kayhannay avatar Dec 16 '23 15:12 kayhannay

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!

dvdsk avatar Dec 16 '23 16:12 dvdsk