Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removing the EventLoop - rebased #354

Merged
merged 42 commits into from Jan 13, 2020

Conversation

mitchmindtree
Copy link
Member

@mitchmindtree mitchmindtree commented Dec 13, 2019

This PR is a rebase of @ishitatsuyuki's work in #301. Originally opened at ishitatsuyuki#3 but closed in favour opening a new PR here.

Backends

  • null
  • ALSA
  • CoreAudio
  • WASAPI
  • Emscripten
  • ASIO

Other TODO

  • Polymorphic struct versions of Traits will be explicitly !Sync, !Send
  • Docs

ALSA multiple streams bug

Edit: This bug seems to have been addressed in the rebase.

After testing each of the examples with the ALSA backend on my Arch Linux system, I noticed that the feedback.rs example no longer works. No audio output was occurring and after some investigation it seems the output stream callback only seems to get called once. beep.rs and record_wav.rs both work fine, which leads me to wonder if there's an issue with the way multiple streams are being handled. feedback.rs also works fine on master, so this has likely been introduced in this PR.

@ishitatsuyuki does anything come to mind as a potential cause for this? I did notice that we're now spawning a whole OS thread per stream. I wonder if there's some kind of race going on in the stream loop? I might see if I can find anything about thread safety in the ALSA docs. I also wonder if we can reduce the number of threads necessary by just using a single thread on which all of the streams run, similar to the old EventLoop::run impl. If there is some race going on, maybe using a single thread for all streams would help to avoid this.

Also, I was reading through PortAudio's non-blocking ALSA code and noticed that, when spinning up a thread for a new stream, they also attempt to set the thread's priority to the highest possible. It might be worth seeing if there is some way we can do this too, though this work can probably wait for a follow-up PR.

@ishitatsuyuki
Copy link
Collaborator

some quick facts:

  • libalsa is thread safe with a flag, which is enabled by default on most distros. In my implementation I assume it is enabled.
  • there are two ways to configure real-time privileges:
    • with ulimits. This is simpler and apparently more popular.
    • with RealtimeKit. It provides additional monitoring to ensure a process doesn't over-use realtime budgets, but requires D-Bus, and because it's not very popular it has been almost a sibling of PulseAudio.

@ishitatsuyuki
Copy link
Collaborator

ishitatsuyuki commented Dec 14, 2019

Oh hey, and I tested the (feedback) examples and apparently it works. I'm running PulseAudio with Arch if that matters.

@mitchmindtree
Copy link
Member Author

Oh strange, it's working for me too now! Even switching through all my different available audio devices seems to work.

Ahh, I can recreate the error by switching back to the non-rebased branch. It looks like one of the ALSA patches introduced in the rebase must have addressed whatever was going on here.

@ishitatsuyuki
Copy link
Collaborator

Feels like #327.

@MOZGIII
Copy link

MOZGIII commented Dec 15, 2019

Polymorphic struct versions of Traits will be explicitly !Sync, !Send

What does this mean (compared to the currently existing event loop APIs)? And why not make them Send + Sync?
I'm asking cause things being Send + Sync is important for my code...

@ishitatsuyuki
Copy link
Collaborator

ishitatsuyuki commented Dec 15, 2019

Polymorphic struct versions of Traits will be explicitly !Sync, !Send ...

@MOZGIII Because it’s not portable. Android is a clear obstacle in this case, and there are also some platforms where thread safety is unknown.

If you need the Send/Sync version, then you need to use the concrete (platform specific) types.

@MOZGIII
Copy link

MOZGIII commented Dec 15, 2019

Oh, ok. I'll still be able to use generics, right?

Are the structs this is about just to abstract away things to dynamic dispatch while hiding it as an implementation detail?

@MOZGIII
Copy link

MOZGIII commented Dec 15, 2019

Actually, it'd be perfect if some lower-level tweaking abilities were exposed if I were to use concrete types - like buffer sizes etc. Any plans on enabling that?

@ishitatsuyuki
Copy link
Collaborator

I was thinking of exposing those platform specific options, but I'm not in charge of development at the moment. It's completely possible and a matter of being implemented or not.

@MOZGIII
Copy link

MOZGIII commented Dec 15, 2019

Sounds good. I'd look into exposing the things I need, but there's way too much turbulence going on with the code base, so I'll just wait for this to land first.

@mitchmindtree
Copy link
Member Author

@msiglreith I just had a read through your ASIO update and it looks good to me!

From my understanding reading through the thread at ishitatsuyuki#2 it looks like the remaining steps are:

  • Ensure Streams don't become invalid if the Device is dropped first. It looks like we can solve this by sharing the Device's driver and asio_streams Arcs with the Streams themselves.
  • Ensure that when a Stream is dropped that its callback is also removed from the main ASIO loop. We could possibly achieve this by having the asio::Driver::set_callback method return a unique Id associated with the callback which can be stored by the Stream. The Id can then be used by the Stream upon drop to remove its callback from the list, perhaps via some new asio::Driver::remove_callback(callback_id) method. It might be worth changing the asio::Driver::set_callback method name to push_callback or add_callback to make its behaviour clearer.
  • Work out some way to stop stream creation/removal from blocking the processing of audio. As there's not an easy, clear solution for this just yet I'm happy to leave this for a future PR, particularly seeing as this is how it was behaving before anyway. Landing this PR is probably a bigger priority as it's a blocker for quite a few other issues.

Does this sound about right? Would you be interested in finishing this off? If not, let me know and I'll rebase and make these changes myself next time I get access to a Windows machine.

@msiglreith
Copy link
Contributor

Hi 👋,

1st and 2nd point are rather design decision imo. It was not clear to me how the lifetime of the objects are coupled in the API. The suggested approach looks viable.

For the 3rd one, there are two aspects:

  • Recreation of the buffers in the asio driver layer when adding a new stream, which could be addressed on the cpal API side
  • Adding/removing streams in the internal event loop, which is not really an issue in my opinion. Using lightweight mutexes should be enough

Would you be interested in finishing this off? If not, let me know and I'll rebase and make these changes myself next time I get access to a Windows machine.

The earliest date for me to start working on it again would most likely be at end of the year.

@mitchmindtree
Copy link
Member Author

Thanks for the quick response!

The earliest date for me to start working on it again would most likely be at end of the year.

No worries! I'll try to get onto this in the next day or two.


// Run for 3 seconds before closing.
println!("Playing for 3 seconds... ");
std::thread::sleep(std::time::Duration::from_secs(3));
drop(input_stream);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why an explicit drop here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream terminates when it's dropped, and this makes it explicit.

// TODO: use AtomicU64 instead
next_stream_id: AtomicUsize,
pub struct Stream {
/// The high-priority audio processing thread calling callbacks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments feel kind of misleading as priority for these threads are never set

//! > **Note**: Creating and running a stream will *not* block the thread. On modern platforms, the
//! > given callback is called by a dedicated, high-priority thread responsible for delivering
//! > audio data to the system's audio device in a timely manner. On older platforms that only
//! > provide a blocking API (e.g. ALSA), CPAL will create a thread in order to consistently
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be noted that it's currently one thread per stream

mitchmindtree and others added 7 commits December 31, 2019 15:45
@mitchmindtree
Copy link
Member Author

mitchmindtree commented Dec 31, 2019

Thanks for the review @msiglreith!

I think this is just about ready to go now other than:

Polymorphic struct versions of Traits will be explicitly !Sync, !Send

@ishitatsuyuki off the top of your head, do you remember if this is necessary for all of those types? Or is it e.g. just the Host that requires this? Or just the Stream? It would be nice if we can make this restriction only where absolutely necessary as it is a bit of an inconvenience to have on the API. Edit: also are you sure we also need !Send? Or is it just !Sync that we need?

@ishitatsuyuki
Copy link
Collaborator

ishitatsuyuki commented Jan 1, 2020

My original intention was AAudio, which is not only not thread safe but also prohibits calling certain functions within a callback. Therefore I went for setting the !Send requirement. (Apparently I can't confirm the callback limits anymore, which used to be explicitly checked in the library code - maybe I'm just missing where it's implemented or Google might have patched this.)

AAudio on the other hand is stateless for stream creation, so the !Send and !Sync bound is only required for Stream.

Though, I think ASIO is not thread safe at all? Therefore we might also need !Sync (but not !Send) on Host and Device.

let buffer_len = frames_available as usize
* stream.bytes_per_frame as usize / sample_size;
let thread =
thread::spawn(move || run_inner(run_context, &mut data_callback, &mut error_callback));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this thread initialize the COM library to COINIT_MULTITHREADED? And, if so, does this mean that the calling thread can have it's requirement on that threading model relaxed since the device setup is less timing-sensitive?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

COM is initialised lazily using thread_local.

Threading model will not be relaxed AFAIK because what matters is the thread that created the handle. As long as the handle is created before the thread spawns, we'll need to use MTA when doing it.

This is in order to ensure consistent restrictions across platforms in a
manner that ensures thread safety across each of the supported
platforms.

Please see added comments in the diff for details on which platforms
impose these restrictions.
This makes some tweaks to the ASIO backend in order to fix some cases
where races may have occured. This should allow us to remove the `Sync`
bound on the `Device` and `Host` types.
Originally this restriction was placed due to uncertainty around the
thread safety of the ASIO API. While the ASIO API itself makes no
thread-safety guarantees that we are aware of, the `asio-sys` high-level
bindings enforce synchronised access to the API and state transitions
via a mutex.
Explicitly make dynamically dispatched API !Send + !Sync
@mitchmindtree
Copy link
Member Author

OK this should be good to go!

@mitchmindtree mitchmindtree merged commit 59ac088 into RustAudio:master Jan 13, 2020
@MOZGIII
Copy link

MOZGIII commented Jan 13, 2020

Any plans on publishing a new version to crates.io in the near future?

@Ralith
Copy link
Contributor

Ralith commented Jan 13, 2020

Thank you for your hard work in bringing this to completion!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants