#How do I control rodio audio playback from different commands?

127 messages Β· Page 1 of 1 (latest)

mellow minnow
#
#[tauri::command]
async fn play(path: &str) -> Result<(), ()> {
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    let sink = Sink::try_new(&stream_handle).unwrap();

    let file = BufReader::new(File::open(path).unwrap());
    // Decode that sound file into a source
    let source = Decoder::new(file).unwrap();

    sink.append(source);

    sink.sleep_until_end();
    Ok(())
}

I have this play code, it works fine, but I want to be able to control the playback from different commands. I've looked for a lot of time, and I can't seem to find it. The most obvious solution would be to somehow make it possible to access the sink from another functions, but I have no idea how to do that. How?

supple laurel
#

A Tauri State is what I would use

#

Something like a PlaybackState

tacit pawn
#

I had to open a thread and communicate with it through mpsc sender in the state

#

is there a better way? ASthink

#

_stream isn't actually used (but still breaks if dropped) and sink (controller for the audio) can be shared with Mutex<Sink>

#

so a way to manually drop _stream later or, well, risking the mem leak and not dropping it would work

#

oh, I guess _stream gets dropped when the song ends, in this example.

#

so for j4ce, Tauri states might work

#

I recommend you look at the links Simon sent, they were very helpful for me eheheheh

supple laurel
#

I just very hastily put something together that should work. Instead of using sink.sleep_until_end(); we store the OutputStreamHandle which should keep it playing, then drop that handle later

use rodio::{source::Source, Decoder, OutputStream, OutputStreamHandle, Sink};
use serde::Serialize;
use std::fs::File;
use std::io::BufReader;
use std::sync::Mutex;
use tauri::{Manager, State};

#[derive(Serialize)]
pub(crate) struct PlaybackState {
  #[serde(skip_serializing)]
  stream: Option<OutputStreamHandle>,
}

impl Default for PlaybackState {
  fn default() -> Self {
    PlaybackState { stream: None }
  }
}

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
  format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command]
async fn play(path: &str, playback_mutex: State<'_, Mutex<PlaybackState>>) -> Result<(), ()> {
  let mut state = playback_mutex.lock().unwrap();
  let (_stream, stream_handle) = OutputStream::try_default().unwrap();
  let sink = Sink::try_new(&stream_handle).unwrap();
  let file = BufReader::new(File::open(path).unwrap());
  let source = Decoder::new(file).unwrap();
  sink.append(source);
  state.stream = Some(stream_handle);
  Ok(())
}

#[tauri::command]
async fn stop(path: &str, playback_mutex: State<'_, Mutex<PlaybackState>>) -> Result<(), ()> {
  let mut state = playback_mutex.lock().unwrap();
  state.stream = None;
  Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
  tauri::Builder::default()
    .plugin(tauri_plugin_window::init())
    .plugin(tauri_plugin_shell::init())
    .invoke_handler(tauri::generate_handler![greet, play, stop])
    .manage(Mutex::new(PlaybackState::default()))
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}
#

Haven't tested it, don't have a file to test it with, but yea, it's Rust, it compiled, it should work x)

tacit pawn
#

the _stream would get dropped when play() returns

#

sadly, from my experience, it needs to be kept alive

#

until the song ends, anyway

supple laurel
#

Guess I'll have to try it then πŸ˜…

#

I never used rodio, what file does it want? Mp3?

tacit pawn
#

mp3 should work

#

but some other formats should also work, as it is designed to be a high-level library on top of opus and other audio low-level libraries.

#

rodio is what caused me to make the PR; it needs libc++ to compile for Android lh_Laugh

supple laurel
#

I pretty much never listen to songs from files on my computer anymore, I just listen on youtube or spotify, looked in my Music folder, and lo and behold I found this, perfect for testing πŸ˜‚

tacit pawn
#

hahahaha

#

I took way too long to learn that the music stops when _stream gets dropped

#

I will be very disappointed if this isn't the case and _stream can be dropped without issues

#

but it worked when I marked it as ManuallyDrop (just for testing)

#

so I'm pretty sure

#

I wanted to ask you this question but it kinda went outside of the scope of Tauri;;

#

sorry for taking over the thread haha

mellow minnow
#

FLACs are great

supple laurel
#

I am so not enjoying rodio right now πŸ˜‚ What kind of library starts a background thread for outputting audio without giving you a handle to that thread, just the ability to sleep until it's done >.<

mellow minnow
mellow minnow
#

I used rodio since it was popular

#

not sure what I'm supposed to use

#

I wanted something that can play FLACs as well as possible

supple laurel
#

There's no "supposed" to use in the Rust end, rodio exists and if that's what you want to use then by god I'll make it work πŸ˜‚

tacit pawn
mellow minnow
#

well then I have to use rodio

#

is sleeping really the only way to play it lol

tacit pawn
#

no, like I've mentioned, I've set it up to open a new thread and communicate with the thread using mpsc

#

however, I feel like Simon can come up with a better solution

mellow minnow
tacit pawn
#

this is my first project on Rust eheheheh

#

Simon has been a huge help

mellow minnow
#

my main issue in rust is organizing my code

tacit pawn
#

yeahh I'm having trouble with that too

mellow minnow
#

also there are some things that in most languages you forget about, but in rust it's a completely separate headache

supple laurel
#
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
use serde::Serialize;
use std::{fs::File, io::BufReader, sync::Mutex};
use tauri::State;

#[derive(Serialize)]
pub(crate) struct PlaybackState {
    #[serde(skip_serializing)]
    stream: OutputStreamHandle,
    #[serde(skip_serializing)]
    sinks: Vec<Sink>,
}

impl PlaybackState {
    pub fn new(stream: OutputStreamHandle) -> Self {
        Self {
            stream,
            sinks: vec![],
        }
    }
}

#[tauri::command]
async fn play(playback_mutex: State<'_, Mutex<PlaybackState>>) -> Result<(), ()> {
    println!("Playing!");
    let path = "test.mp3";
    let mut state = playback_mutex.lock().unwrap();
    let sink = Sink::try_new(&state.stream).unwrap();
    let file = BufReader::new(File::open(path).unwrap());
    let source = Decoder::new(file).unwrap();
    sink.append(source);
    state.sinks.push(sink);
    Ok(())
}

#[tauri::command]
async fn stop(playback_mutex: State<'_, Mutex<PlaybackState>>) -> Result<(), ()> {
    println!("Stopping!");
    let mut state = playback_mutex.lock().unwrap();
    state.sinks.clear();
    Ok(())
}

fn main() {
    let mut stream = OutputStream::try_default().unwrap();
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![play, stop])
        .manage(Mutex::new(PlaybackState::new(stream.1)))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
#

There, I think that's gonna be my "official" solution

#

OutputStream doesn't implement Send? Well fuck you too rodio because now we're not moving it and instead using it more aligned with how it's "intended" to be used πŸ™‚

#

Get an output stream handle to a physical device. For example, get a stream to the system’s default sound device with OutputStream::try_default()
Since OutputStream is a handle to a device, not a handle to a specific playback, it actually makes a whole lot more sense to store a single version of it globally instead of creating it inside the command

mellow minnow
#

bro a magician

tacit pawn
#

true, but what if the user would like to change the output device eheheheh

mellow minnow
#

good point

supple laurel
#

Here's a gun, use it on the users that want another deviceπŸ”«

tacit pawn
supple laurel
#

Thinking about how I can fix this intently

tacit pawn
mellow minnow
#

there is

#

with webrtc?

#

I know that's possible

#

but does that tamper with the audio quality in any way

tacit pawn
#

JavaScript/TypeScript has much more developed audio libraries

mellow minnow
#

shady js business

#

and I use lossless files

mellow minnow
#

in js

tacit pawn
#

hmmm...

supple laurel
#

It's certainly easier to just move the audio into the frontend and use the asset protocol for fetching files for example, but yea, if you're a real audiophile you can totally hear the different i can't hear the difference at all

tacit pawn
#

I saw a Rust music app that intentionally moves music to JavaScript/TypeScript for it to be handled there

mellow minnow
tacit pawn
#

but if there is a difference, I see why Rust is preferable

#

I'll keep that in mind with my project as well

#

I guess threads and mpsc might be the way to do it?

supple laurel
#

Threads and mpsc is definitely one way to do it, I just find it tedious to deal with and am trying to think of a simpler solution πŸ™‚

tacit pawn
#

same

#

very tedious eheheheh

mellow minnow
#

me when rust πŸ€•

tacit pawn
#

eh, I think the issue's more with rodio/cpal's OutputStream

mellow minnow
#

I just suck at rust

tacit pawn
#

yeah, I'm a beginner as well

#

especially since this is pretty much my first non-interpreted language without garbage collectors

supple laurel
#

I am this close to developing my own audio library

#

Come to think of it, why aren't I developing an audio plugin for Tauri? πŸ€”

tacit pawn
#

that would be super cool SayoriFingerguns

#

but, the reason why rodio exists is...

#

well, most low-level audio libraries have limited cross-platform support, from what I've seen

mellow minnow
#

Oops wrong reply

tacit pawn
#

haha

#

I saw a open source music client made in Rust, and they had a lot of libraries just for the audio

#

portaudio and jack (requires system lib, like with apt install (I think))
alsa for linux audio (also system lib)
rodio for windows/mac/linux
gstreamer for streaming audio

#

and that's without mobile support too
just straight up not ideal

#

rodio is bad for mobile too, as it requires a workaround and libc++ for Android

supple laurel
#

Yea there's always gonna be a lot of libraries you gotta support when you start dealing with cross platform, but currently I'm like, the time I'm putting into circumventing this OutputStream not implementing Send is the same time it'd take me to get pretty far on a plugin to handle it x)

But yea currently while I do think the solution I sent before is pretty neat, it doesn't support switching devices, so if you need switching devices all I can think of is using mpsc. Even tried to be clever with using Tauri events and realized pretty fast that yea no that still involves moving between threads

Since the value has to exist in a single thread there really is no way around it, inter thread communication has to take place, and the way to do that is with mpsc

tacit pawn
#

that's a shame

#

but it is what it is

#

if you do make an audio plugin, I'll be sure to use it eheheheh

#

if we have to use mpsc, maybe it can still be improved with stuff like Tokio

#

I mean, the thread only has to hold on to the _stream variable

#

and then await for a tokio::sync::oneshot

#

which then it will drop the variable?

#

since Sink only needs stream_handle to initialize, which can be sent between threads?

mellow minnow
supple laurel
#
#[derive(Serialize)]
pub(crate) struct PlaybackState {
    #[serde(skip_serializing)]
    stream: Option<OutputStreamHandle>,
    #[serde(skip_serializing)]
    sinks: Vec<Sink>,
    #[serde(skip_serializing)]
    sender: Sender<String>,
}
impl PlaybackState {
    pub fn new(stream: Option<OutputStreamHandle>, tx: Sender<String>) -> Self {
        let _ = tx.send("default device".to_string());
        Self {
            stream,
            sinks: vec![],
            sender: tx,
        }
    }
}

#[tauri::command]
async fn change_device(
    device: String,
    playback_mutex: State<'_, Mutex<PlaybackState>>,
) -> Result<(), ()> {
    println!("Changing device!");
    let mut state = playback_mutex.lock().unwrap();
    let _ = state.sender.send(device);
    Ok(())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![play, stop, change_device])
        .setup(move |app| {
            let app = app.handle();
            let (tx, rx) = std::sync::mpsc::channel::<String>();
            app.manage(Mutex::new(PlaybackState::new(None, tx)));
            std::thread::spawn(move || {
                let mut stream = OutputStream::try_default().unwrap();
                // Receive messages in a blocking loop
                while let Ok(message) = rx.recv() {
                    println!("Received: {}", message);
                    stream = OutputStream::try_default().unwrap();
                    let state_mutex = app.state::<Mutex<PlaybackState>>();
                    let mut state = state_mutex.lock().unwrap();
                    state.stream = Some(stream.1);
                }
                let state_mutex = app.state::<Mutex<PlaybackState>>();
                let state = state_mutex.lock().unwrap();
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
mellow minnow
#

what. did you do

supple laurel
#

I had to remove some stuff because of Discord limit on characters

mellow minnow
#

you're insane

tacit pawn
#

my god debaanChrom

supple laurel
#

But yea this uses mpsc, so it annoys me a bit, but all you gotta do now is use the sender.send("DEVICE NAME") part to switch device

#

Ofc I have no idea how rodio actually switches to a specific device but I'm guessing device name might be used? πŸ˜…

#

So now it's just a matter of displaying to the user all the available devices and have them send sufficient info through mpsc for it to set up the streaming device

mellow minnow
#

wow!

supple laurel
mellow minnow
#

I love test.mp3 lmfao