freesig / cpal

Audio player in pure Rust
0 stars 0 forks source link

Simultaneous Input and Output Stream #13

Closed freesig closed 5 years ago

freesig commented 5 years ago

So I just tried making an "echo" example in Nannou with ASIO. It just records the input buffer and puts it into the output. However only the first stream will start. Which ever one you call build on first. I'll start investigating why this is.

So it looks like the first call to AsioDriver::new() creates the drivers. It is stored inside the AsioStream.

pub struct AsioStream {
    driver: ai::AsioDrivers,
   ...
}

So we just need to share this between the different active streams. Maybe like

driver: Arc<Mutex<Option<ai::AsioDrivers>>>,
freesig commented 5 years ago

Actually I think the reason this isn't working is just because of this:

        let mut asio_drivers = ai::AsioDrivers::new();
        let load_result = asio_drivers.loadDriver(raw);
        // Take back ownership
        my_driver_name = CString::from_raw(raw);
        if !load_result {
            return Err(ASIOError::DriverLoadError);
        }

Where "raw" is the name of the driver. So it's getting back false from loadDriver but that's only because it's already active not because it failed to load.

freesig commented 5 years ago

Actually we do need to store the AsioDriver once it is initialized because the destructor for this class removes the driver.

AsioDrivers::~AsioDrivers()
{
    removeCurrentDriver();
}

Todo

freesig commented 5 years ago

Ok I still have the issue of how to share this AsioDriver between streams. I can't change the way that streams are created. Consider this:

    let output_stream = app.audio.new_output_stream(output_model, audio_out).build().unwrap();
    let input_stream = app.audio.new_input_stream(input_model, audio_in).build().unwrap();

The AsioDriver is inside both the output_stream and input_stream. I need some way to share it without changing the api.

Idealy it would happen in the EventLoop::new() But this would require some sort of global which is pretty bad.

freesig commented 5 years ago

Ok I've done a bit of a rewrite and now the drivers live in the device. I just need a way to share them across streams and this issue will be fixed.

Ideas

  1. Put the drivers in some sort of mutable static. This is a bad idea, hopefully there's a better way
  2. Run another thread that handles the driver creation and destruction. Better that 1. but still not ideal
mitchmindtree commented 5 years ago

Yeah I think the way global state is normally managed in low level crates like this is using a lazy_static! with a Mutex around it. This way at least you can't accidentally cause any data races.

I think it's good to avoid spawning threads in low-level crates like this when possible as native threads can be pretty expensive on some platforms. E.g. I think on macOS the default native thread stack size is like 2MB that needs to be allocated. You can adjust the stack size manually, but anyways I think an extra thread might be overkill for this case.

freesig commented 5 years ago

no worries, I'll give that a go now.

freesig commented 5 years ago

So the way I had this working was that a Drivers struct was created that holds the drivers. Then you call the functions through that struct. This ensures you can't call a function that needs an active driver without the driver being active. However with a lazy_static Mutex you end up in this really awkward place where you need to check the value is active with every call like this:

fn get_channels() -> Result<(), AsioError> {
match *ASIO_DRIVERS.lock().unwrap() {
Some(_) => {
//Do some work
Ok(())
}
None => Err( NoDriverError )
}
}

It's pretty messy and error prone. Like if someone adds a new function and forgets to add this then we have a problem. Am I missing something or is this necessary?

freesig commented 5 years ago

Actually I think I found an ok solution

freesig commented 5 years ago

This still doesn't work with Nannou. I think something in Nannou is dropping the output stream when an input stream is created

mitchmindtree commented 5 years ago

It seems strange that this would be the case when nannou doesn't have any platform or OS specific code, and simultaneous input/output works with the other backends. I made a CPAL feedback example for testing this a little while back - it might be useful to test that it works for sure in CPAL itself first?