-
Notifications
You must be signed in to change notification settings - Fork 322
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
no-exceptions: Synchronously returning error messages? Distinguishing between error callbacks on main and audio thread? #337
Comments
Let me first say that the no-exceptions approach is new and I would not be surprised if it needs to be tweaked a bit. So, I'm happy to take suggestions or PRs that fix identified problems. I would also mention that I'm working on a big update regarding device enumeration (in the newdeviceselection branch), so that may change some behaviour too. But regarding what is described in this post, I don't immediately see what the problem is. It is true that the callback thread for the ALSA API is started near the end of the RtApiAlsa::probeDeviceOpen() and then conditionally waits until the stream is started, but that does not block other RtAudio functions from being called. If an error occurs when startStream() is called, the error() function (and if set, the error callback) will be invoked. |
The issue I have is that there's no longer a clean way to call
EDIT: Possibly related to picking a device: #194 |
Perhaps I'm missing something but if openStream fails, it will return an RtAudioErrorType, so it would be clear there was a problem in that specific function (and if openStream fails, no audio thread would be created). I agree it might be an issue to get the corresponding error text for a specific problem, though it should be the one passed to the error callback with that particular error type. If this is the problematic issue for you, maybe an extra function such as getLastErrorMessage() could be added? |
There are two high-level approaches to error handling: handling UI thread errors but ignoring audio thread errors (ideally they'd just stop the stream rather than terminating the app), and handling both UI thread errors and audio thread errors in the same or different ways. Currently this can be done by adding an error callback in both cases, and checking if the current thread is different from the thread calling There are two primary cases I feel aren't well-served right now: handling UI thread errors without a callback but ignoring audio thread errors, and handling UI thread errors without a callback but handling audio thread errors with a callback (you don't have much choice). If you add One property I want the API to have, is that when I add or remove audio thread error handling from an app, I don't have to change or move the UI thread error handling code. One way to do this is to separate audio thread error callbacks from UI thread error handling (and this is the API I want at the moment). For UI thread error handling (and synchronous code in general), I still prefer return codes and error strings over callbacks, since callbacks are indirect control flow, which is complicated to write and understand. Avoiding data races by making shared state explicitAlso, can an unlucky user cause RtAudio to write to (Why is If you split the audio and UI thread error functions, the tricky part is making sure you only call the audio thread error function on the audio thread, and the UI thread error function on the UI thread. Personally I like to ensure this by creating a separate object for audio thread state (eg. the ALSA/Pulse handle, the current audio error string, plus a pointer to shared state), object for UI thread state (not sure what, plus a pointer to shared state), and struct for shared state (eg. an atomic variable holding playback state, variables to signal the audio thread to pause/resume/stop playback, probably put mutexes and condvars here too). The audio thread object is constructed on the UI thread, but sent to the audio thread and no longer referenced by the UI thread, and the two objects only communicate through the shared state struct. Then you put methods on the audio and UI thread objects, so each object's methods only has access to the state belonging to that thread plus shared state, and accesses to shared state are syntactically distinguished since it's behind a pointer. This is the approach Rust encourages for threading, and it makes it clear what methods run on each thread and what state each thread accesses, by baking them into the type system and explicitly in the source code (instead of relying on the programmers understanding the code and not making mistakes, which is rarely the case). However this isn't standard C++ practice (I don't care, I prefer correct-by-design to standard practice) and C++ isn't the best at mutexes (see my blog post), and it's a lot of work to retrofit existing C++ code into this style (as a consolation, it's good at exposing data races to be fixed), and requires intimate code knowledge I don't have yet (since I didn't design RtAudio). |
Will you accept a pull request to split I looked into porting, and unfortunately it seems to require a good deal of effort, and may conflict with the in-progress changes at https://github.com/thestk/rtaudio/tree/newdeviceselection. Also I only have a Windows and Linux machine to build and test on, so I can't port Core Audio at the moment, and I don't use OSS on Linux, nor ASIO on Windows (unless I perhaps install ASIO4ALL?). I don't know if this approach allows me to incrementally port code to the new "thread-safe by design through static typing" architecture. |
I spent a day or two sorting through RtAudio's ALSA backend. I see a bunch of data races ( Unfortunately RtAudio's threading is difficult to understand, with In comparison, cpal (a Rust library)'s multithreading (link) is beautiful. Every type is either owned by the main thread ( Am I actually qualified to rewrite RtAudio multithreading? I'd have to change data structures, and I can't port or test Core Audio or OSS (and perhaps ASIO too). I also also want to add a mode where RtAudio tracks the default device as it changes. This is unrelated to threading, and I could work on that while leaving the data races in place. But I feel uncomfortable trying to add extra features on a broken library, so I don't know what to do... |
I'm happy to have contributions to RtAudio. However, I would point out that RtAudio has been working fairly well for two decades. As messy as you may think it is, I don't want to break it. AND importantly, I want to keep things simple. So, you can submit PRs but I'm not interested in complete rewrites unless they make sense and hopefully reduce the number of lines of code. |
Also, I'm not interested in having RtApi split. And I would look to the new branch (newdeviceselection) as a basis, as I will merge that as soon as I finish getting it to work for the Windows APIs. I would note that in implementing this new branch, I have generally been able to reduce the lines of code fairly significantly. |
If you're not interested in splitting RtApi (I'm not exactly sure what you mean), I might fork RtAudio (and largely preserve API but not architecture or ABI), and test my changes using BambooTracker. Though I'll wait until newdeviceselection is finished or merged, before working more on this. Using cpal as a reference, I estimate my proposed changes should maintain or decrease the lines of code. |
I've finished the newdeviceselection port but it entails more API changes, so I am hoping to get feedback from others before merging it. Generally, I think it is a big improvement in terms of how devices are queried and enumerated. |
I haven't done any serious work with device selection in exotracker. I might look into porting https://github.com/BambooTracker/BambooTracker to updated RtAudio, but I'm occupied elsewhere now. If I have the time (unlikely), I still hope to fork RtAudio and rewrite a good chunk of the code from scratch. I've learned a lot about ALSA buffering (there are substantial changes I'd make to RtApiAlsa's backend code), but have yet to study PulseAudio or WASAPI and friends. |
Sorry for not responding earlier. After months of rewrites, I finally have time to try upgrading RtAudio and seeing how it behaves. How does newdeviceselection compare to master right now (master was updated more recently)? |
There may have been a few recent updates to master recently but newdeviceselection is the future. I just need to find time to merge it into master. |
Playing with the newdeviceselection branch. Here's my preliminary feedback:
Redundant device probing:
|
Observations from reading the ID system more carefully (only RtApiAlsa so far):
|
Thanks for these observations and suggestions. I'm not sure when I'll have time to carefully consider them but I'm very happy to have the feedback. Currently, my approach was not to expect the user to specify a rescan but rather to do it automatically whenever a call is made that might need to involve a re-probing of devices (given hot-pluggable devices). If the APIs made it easier to know when new devices were added, things could be simplified but that is not currently the case. |
The suggestion I'm most strongly for, is to remove My other suggestions are less important. I have not observed two different devices with the same name so far (since RtApiAlsa looping over cards and device matches the final discovered list). And from what I hear, |
I looked into updating an existing RtAudio app to no-exceptions master, and ran into some trouble (BambooTracker/BambooTracker#426 (comment)). This is preliminary and I didn't try actually fixing the code, mostly skimming the RtAudio code and diffs to plan out what I'd have to change. Sorry if I'm misunderstanding something that's already documented.
Currently, when
audio->openStream()
fails, BambooTracker shows a dialog with contents taken fromRtAudioError::getMessage()
. After the merge,RtAudioError
was removed,audio->openStream()
only returns an enum code (no error message string), andRtApi :: error()
only reports the message by calling thestd::function<...> RtApi::errorCallback_
callback. To my knowledge, you can't access the protectedRtApi::errorText_
field without patching RtAudio to expose it through a getter or similar.If you upgrade to no-exceptions and want to keep printing error messages, (I think) you would have to set a callback. The issue is that
RtApi :: error()
(which calls your callback) gets called on both on the UI and audio threads, and your callback needs to act differently based on which thread it's running on.One idea is to edit
AudioStreamRtAudio::initialize()
, replace the start of thetry
block withaudio->setErrorCallback(...);
, replace the end of the block withaudio->setErrorCallback({});
to reset it, and pray that the audio thread doesn't start and callRtApi :: error()
before we callsetErrorCallback({})
. I'm not sure that's the case.If I understand correctly,
RtApi :: openStream()
calls (eg.)RtApiAlsa :: probeDeviceOpen()
which callspthread_create
(which as far as I can tell, begins execution immediately, rather than suspending the thread). In the case of ALSA,alsaCallbackHandler
callsRtApiAlsa :: callbackEvent()
which (in case ofSTREAM_STOPPED
) waits forAlsaHandle::runnable
to become true (it's initially false). I think RtAudio defaults toSTREAM_STOPPED
. As a result,RtApiAlsa
can't continue executing and callRtApi :: error()
before the stream is started.I'm not sure if this will remain the case, and I didn't study every other backend to ensure they share this property (that if you call
RtApi::setErrorCallback({})
whenRtAudio :: openStream()
returns, thenRtApi :: error()
cannot possibly be called on the newly spawned audio thread). I thinkRtApiPulse
is the same way.If (in theory) the audio thread starts up, but immediately fails and calls
RtApi :: error()
, there's an endless zoo of race conditions between the GUI thread callingRtApi::setErrorCallback({})
and resettingerrorCallback_
, the audio thread readingerrorCallback_
to check whether to call it (this is a data race), and the audio thread callingerrorCallback_
which tries to show a message box on the wrong thread (cue firework explosions).One possible way out is to set a callback which checks the current thread ID and only opens a message box if running on the GUI thread.
The text was updated successfully, but these errors were encountered: