RustAudio / cpal

Cross-platform audio I/O library in pure Rust
Apache License 2.0
2.67k stars 352 forks source link

[ASIO] bug/strange behaviour with threads - only one thread can access devices #798

Open the-drunk-coder opened 1 year ago

the-drunk-coder commented 1 year ago

Here's an interesting example (assuming Windows 11 + ASIO4ALLv2 as a platform):

use std::thread;
use anyhow;
use cpal::{
    traits::{DeviceTrait, HostTrait},
};

fn main() -> anyhow::Result<()> {

    let host = cpal::host_from_id(cpal::HostId::Asio).unwrap();

    // the problem doesn't occur with Wasapi, neither with other
    // hosts on other platforms ...
    // let host = cpal::host_from_id(cpal::HostId::Wasapi).unwrap();

    // if this loop is commented out, the spawned thread finds the "ASIO4ALL v2" device,
    // otherwise the list is empty ...
    for dev in host.output_devices()? {
    println!("MAIN THREAD - found device {}", dev.name().unwrap());
    }

    let handler = thread::spawn(move || {
    if let Ok(devices) = host.output_devices() {
        let mut dev_count = 0;
        for dev in devices {
        dev_count += 1;
        println!("SPAWNED THREAD - found device {}", dev.name().unwrap());
        }
        println!("SPAWNED THREAD - found {dev_count} devices");
    } else {
        println!("SPAWNED THREAD - can't find devices");
    }   
    });

    handler.join().unwrap();

    Ok(())
}

I'd expect (as this doesn't seem to be a problem at all with other hosts) that both the main thread and the spawned thread can access the list of devices.

Turns out, this isn't the case. A list of devices is returned, but it's empty. If I comment out the statements in the main thread, the spawned thread can find all devices without problems.

Within the same thread, I can access the list of devices as often as I want.

I can't really explain this ... does anyone have a hint?

Best, N

Ralith commented 1 year ago

Seems like it must be a quirk of the ASIO API; the cpal ASIO host doesn't seem to have any spooky global state.

the-drunk-coder commented 1 year ago

Hmm seems to have to do more with the device state rather than the driver state (if that's the correct terminology?).

This variant works:

use std::thread;
use anyhow;
use cpal::{
    traits::{DeviceTrait, HostTrait},
};

fn main() -> anyhow::Result<()> {

    let asio = asio_sys::Asio::new();

    for name in asio.driver_names() {
        println!("MAIN THREAD - found {}", name);
    }

    let handler = thread::spawn(move || {
    let spawned_thread_host = cpal::host_from_id(cpal::HostId::Asio).unwrap();

    if let Ok(devices) = spawned_thread_host.output_devices() {
        let mut dev_count = 0;
        for dev in devices {
        dev_count += 1;
        println!("SPAWNED THREAD - found device {}", dev.name().unwrap());
        }
        println!("SPAWNED THREAD - found {dev_count} devices");
    } else {
        println!("SPAWNED THREAD - can't find devices");
    }       
    });

    handler.join().unwrap();

    Ok(())
}

So as a workaround, you can get the available device names from asio directly (not wrapped in a cpal host/device), and then pick the device in a different thread ...