freesig / cpal

Audio player in pure Rust
0 stars 0 forks source link

Limitation of one input and one output stream #21

Closed freesig closed 5 years ago

freesig commented 5 years ago

Problem This is no longer relevant due to the comments bellow

When constructing an AsioStream ASIO takes a pointer to the buffer_switch function:

extern "C" fn buffer_switch_output(double_buffer_index: c_long, direct_process: c_long) -> () {
    let mut bc = buffer_callback.lock().unwrap();

    if let Some(ref mut bc) = bc[0] {
        bc.run(double_buffer_index);
    }
}

Note this can't be a closure. However we need a buffer_switch per stream. They access the buffer_callback via a static like this:

struct BufferCallback(Box<FnMut(i32) + Send>);

lazy_static! {
    static ref buffer_callback: Mutex<[Option<BufferCallback>; 2]> = Mutex::new([None, None]);
}

The reason for this is that cpal needs access to the buffer_callback so that it can set it. But ASIO also needs access to it via the function buffer_switch callback so that it can call it each tick.

I can't think of anyway to change this to be a Vec instead of an array because you need a buffer_switch function for each buffer_callback. The buffer_switch needs to be hard coded to call the right buffer_callback. Note the 0 in this line:

if let Some(ref mut bc) = bc[0] {

I can't use a closure to capture which index it is because closure can't be plain function pointers (required by ASIO).

I can imagine that this could be possible using a macro to generate all the buffer_switch functions. I think this would need to happen at compile time and therefor we would need to have some sort of upper_limit on the amount of streams.

AsioStreams

I should clarify here that an AsioStream will contain all of your channels for an input or an output. For example with a single ASIO device there would be no way to create multiple output streams to the same device because each time you create a new output stream the buffers would get reallocated and the old ones would be dropped. The same is true for input. However if you were trying to connect to multiple different ASIO devices then this issue would limit you to a single device.

freesig commented 5 years ago

Single Buffer Switch

On further investigation it appears ASIO will only ever call a single buffer_switch function. On top of that all input and output buffers should actually be allocated at the exact same time via ASIOError ASIOCreateBuffers(ASIOBufferInfo *bufferI nfos, long numChannels, long bufferSize, ASIOCallbacks *callbacks); Any subsequent calls to this function actually overwrite old calls. This is going to require a decent rewrite. I also don't think it will be possible to have mutiple input and output streams.

Ideas

  1. Everytime ASIOCreateBuffers is called then get all previous buffers and reallocate them. Ie each time a stream is added all previous steams are reallocated.
  2. Don't allocate anything until all streams are set. I'm not sure if this would be feasible with the current API because a user is free to add a stream at anytime.
  3. Allocate a single Input and Output stream anytime either build_input_stream or build_output_stream is called and then store them. This would allow us to allocate just once and then make the buffers available as needed. I'm not sure what problems we would have if they want to use less channels or a different format. Also a user might not want an input stream for example and they would get one by default. This would be a waste of buffers on a resource scarce systems.

TL/DR I made the assumption that calls to ASIO were independently associated with an input or an output but it seems they use a one big global call for everything approach 👎

freesig commented 5 years ago

Thoughts

This means there is no way to have multiple streams to an ASIO driver. Infact it doesn't really make sense. We basically have zero or one input and zero or one output.

freesig commented 5 years ago

I'm not sure how we make this clear to the user of CPAL though because they could call build_output_stream() twice and it will implicitly destroy the first stream

freesig commented 5 years ago

I think to clarify this a bit more we can look at what a stream is in CPAL vs ASIO. CPAL A stream is

ASIO A stream is

For example:

let asio_stream = Vec<ASIOBufferInfo>;
struct ASIOBufferInfo {
input: bool
channel_index: usize,
double_buffer: *mut [f32; 2],
}

Note here that the asio_stream is a mixture of input and output buffers where as the cpal has a seperate stream for input and output. CPAL also supports multiple streams where ASIO only supports a single stream of combined input and output