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

Add AAudio backend #559

Merged
merged 15 commits into from
Oct 28, 2020
Merged

Add AAudio backend #559

merged 15 commits into from
Oct 28, 2020

Conversation

nyorain
Copy link
Contributor

@nyorain nyorain commented Dec 6, 2019

This implements a first AAudio backend with support for input, output and duplex streams. It uses callbacks for best latency and is completely lock-free in the audio thread (as the aaudio docs recommend). I could only test it on one device. Some additional functions could be implemented via guesswork, jni or other APIs (see the SLES backend), I didn't do that for now and would prefer to keep this PR down to a first basic implementation. There are a couple of NOTEs in the code in places that could need improvements or at stuff I wasn't sure about. I will continue to fix out minor details but am already thankful for input, especially regarding the general backend design.
The cubeb docs don't state this so i assumed it's ok if the state callback is called from a different
thread than the audio callback and that they don't have to be mutually exclusive, i.e. can be called at the same time. Is that correct?
We furthermore currently send a callback with STATE_STOPPED some time after STATE_DRAINED since that simplifies the backend state logic and isn't strictly wrong. Does that break expected behavior? That could be changed without much work.
The backend is currently not used over SLES (i.e. never unless explicitly requested) but i guess this should be changed when it's well tested as it can be expected to provide better latency.

@achronop
Copy link
Contributor

achronop commented Dec 6, 2019

Thank you for that. It needs more iterations since it's big and I need to catch up with the AAudio specifics. For now, I am answering some of your questions

The cubeb docs don't state this so i assumed it's ok if the state callback is called from a different
thread than the audio callback and that they don't have to be mutually exclusive, i.e. can be called at the same time. Is that correct?

That's correct. However, we need to guarantee that the data_callback is not triggered after a drain or stop. The audio thread might still be cycling but the stm->data_callback must not be called again. That means, a client must not see another callback after a drained data_callback has returned, or must not see another callback after the cubeb_stream_stop() has returned. Regarding the state callback the closer to the state change event it fires the better. Just so you know on other platforms we fire the drain callback from the audio thread, not the best practice though.

We furthermore currently send a callback with STATE_STOPPED some time after STATE_DRAINED since that simplifies the backend state logic and isn't strictly wrong. Does that break expected behavior? That could be changed without much work.

Yes. It's not a big problem but other backends do not do that so it's better to follow the same behavior here and not fire the stop event on drain. A cubeb client must call explicitly cubeb_stream_stop() in order to receive a stop state callback even if the stream is stopped internally due to the drain.

@nyorain
Copy link
Contributor Author

nyorain commented Dec 7, 2019

It needs more iterations since it's big and I need to catch up with the AAudio specifics

There's no hurry. I'm still fixing up some details as well.

However, we need to guarantee that the data_callback is not triggered after a drain or stop. The audio thread might still be cycling but the stm->data_callback must not be called again. That means, a client must not see another callback after a drained data_callback has returned, or must not see another callback after the cubeb_stream_stop() has returned.

Makes sense, this is already guaranteed in the backend.

It's not a big problem but other backends do not do that so it's better to follow the same behavior here and not fire the stop event on drain.

Okay, I changed that.

@achronop
Copy link
Contributor

achronop commented Dec 9, 2019

Regarding the supported methods I would say that we need to support at least the methods that the OpenSL ES backend supports (check here). From a quick look, we are missing only two.

  .stream_get_latency
  .stream_set_volume

Do you think we can add support to those easily?

I am also adding more people to look at it.

Thank you!

@nyorain
Copy link
Contributor Author

nyorain commented Dec 9, 2019

AAudio has no volume concept but stream_set_volume should be easy to implement by manually multiplying each sample with the volume just like e.g. the alsa backend does it. I will add that. stream_get_latency is harder, we have to employ guesswork based on the buffer sizes or the jni/media library way that it's done in the OpenSLES backend.

@nyorain
Copy link
Contributor Author

nyorain commented Dec 9, 2019

For clarification: we can implement stream_get_latency natively once the stream is running, returning the actual latency of the last written/read data. But that will vary over time, e.g. grow after a callback gets triggered and then get reduced over time again. And we can't predict that before the stream is started. Is that a valid implementation of the function?

@achronop
Copy link
Contributor

Is that a valid implementation of the function?

Hmm I don't think that this is what we want here. I believe the jni stuff we do on opensl es are adequate and relatively easy to import. That said, I am not sure how accurate the guessing can be. If you think that it can be good enough, please let us know, to consider it.

@padenot
Copy link
Collaborator

padenot commented Dec 10, 2019

@nyorain, thank you very much for doing this!

This member is for doing A/V sync, it needs to be the time it take between returning from a callback that has output data to the time one hears this buffer physically. It's a static number, for a particular stream setting and device configuration (but is expected to change if the underlying device changes).

As you can see, this is not the prettiest code, but has been determined to answer a correct number for a variety of use cases: Bluetooth device using hands-free, Bluetooth device using a2dp, normal wired headphones, speakers, external card, etc., and provide good A/V sync there (also for reporting the latency via AudioContext.outputLatency so people doing their own visuals can be synchronized).

We can certainly add it after merging this however, you've done enough work here :-).

The volume is simpler, and we sometimes implement it in software as you saw. On some backends, it's implemented using OS APIs, this allows a particular scenario to work better:

  • Play at volume 1.0
  • Pause
  • Change the volume to 0.1
  • Play

If implemented in the cubeb backend, and depending on the OS, etc., this can produce a burst of audio at 1.0 (because it's still buffered somewhere in the OS), and then play at 0.1 volume. Using the APIs from the OS solves this (since the volume is applied later in the pipeline).

Implementing in software is what we should do here I think.

Thanks again for working on this!

@nyorain
Copy link
Contributor Author

nyorain commented Dec 10, 2019

You're welcome, I'm using this library in my own projects and it works quite well, so thanks for writing it in the first place!

If implemented in the cubeb backend, and depending on the OS, etc., this can produce a burst of audio at 1.0 (because it's still buffered somewhere in the OS), and then play at 0.1 volume. Using the APIs from the OS solves this (since the volume is applied later in the pipeline).

The reasoning for including the volume api in cubeb makes sense, I added a software implementation. With AAudio, the scenario you describe shouldn't be a problem (at least not if the restarting of the stream happens after the state callback was called with STOPPED) since stopping an output stream will still first output all buffered data.

We can certainly add it after merging this however, you've done enough work here :-).

I don't use the latency functionality or A/V sync and wouldn't be sure how to properly test that the jni latency functions return valid values for this backend so I guess it's indeed the best to move this to a separate PR.

@nyorain nyorain changed the title [WIP] Add AAudio backend Add AAudio backend Dec 10, 2019
// NOTE: not sure about this.
// But when this stream contains input and output stream, their
// sample rates have to match, don't they?
assert(!target_sample_rate || target_sample_rate == input_stream_params->rate);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, this is guaranteed by the function calling this.

Copy link
Collaborator

@padenot padenot left a comment

Choose a reason for hiding this comment

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

Do you think this is stable enough so that I can start reviewing it proper? I see you've just added a commit.

@@ -217,6 +224,11 @@ cubeb_init(cubeb ** context, char const * context_name, char const * backend_nam
#endif
#if defined(USE_OPENSL)
opensl_init,
#endif
// TODO: should probably be preferred over OpenSLES when available.
// Initialization will fail on old android devices.
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can change the order to do this: if a particular function fails, the following ones will be called, but if one succeeds, this function returns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I did that for testing and for my own cubeb builds. But should it already be done here at this stage of the backend?

// The DRAINING state means that we want to stop the streams but
// may not have done so yet.
// The aaudio docs state that returning STOP from the callback isn't
// enough, the stream has to be stopped from another threads
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: s/threads/thread/

@nyorain
Copy link
Contributor Author

nyorain commented Dec 16, 2019

Yes, this is ready for review. Just one minor thing left (that i know of): there currently can be crashes (failed ringbuffer assertion) when restarting an app with this backend since we use ALOGV. That seems to use a function-static/global variable and those are not always reset when the app restarts on android (but the audio thread will change obviously, triggering the invalid thread_id assertion in the async logger ringbuffer). I'll either have to remove the calls to ALOGV or explicitly reset the thread ids in the static ringbuffer in aaudio_init somehow. If we just consider ALOGV broken on android and move on (it's equally broken for the other android backends, note how it's never used there), that should at least be documented somewhere I guess.

If you find the current threaded solution for handling stream state too complicated (using an additional notifier thread), I could revert a6422f6. See the commit message for more details (and a link to a post on the problem I did). Before this commit, we just used constant looping with short sleeping in between.

@achronop
Copy link
Contributor

If we just consider ALOGV broken on android and move on (it's equally broken for the other android backends, note how it's never used there), that should at least be documented somewhere I guess.

Yeah, don't spend too muh time with that. If you have a handy solution go for it otherwise just remove the call and go on. The logic is not broken but there are limitations and they are not your fault. Please, document it as you said.

Copy link
Contributor

@achronop achronop left a comment

Choose a reason for hiding this comment

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

Created Bug 1609372 that will track the importing of the backend.

@@ -0,0 +1,1373 @@
/* ex: set tabstop=2 shiftwidth=2 expandtab:
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be possible to turn it to c++ instead of c.

new_state = STREAM_STATE_STOPPED;
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We can remove that line.

@achronop
Copy link
Contributor

Sorry, my review is not ready. I meant to make an individual comment about the bug. The two comments above are real but more will come.

@achronop
Copy link
Contributor

@nyorain, I imported the backend in firefox and attempted a youtube session. It crashed any idea what this night be? How did you test locally?

@achronop achronop self-requested a review January 15, 2020 11:24
@nyorain
Copy link
Contributor Author

nyorain commented Jan 16, 2020

The backend was tested with my own dummy audio apps (that cover all cases like stop, drain, input + output or seperate streams). I could look into setting up a build environment for firefox on android but I expect that to be a lot of work, not sure when I'll have time for that tbh. Without symbols or android log I have no idea what the issue is, unfortunately.

@achronop
Copy link
Contributor

The backend was tested with my own dummy audio apps (that cover all cases like stop, drain, input + output or seperate streams).

Is it something that you can send me privately and speed up my testing?

I could look into setting up a build environment for firefox on android but I expect that to be a lot of work, not sure when I'll have time for that tbh. Without symbols or android log I have no idea what the issue is, unfortunately.

No need to spend time on that. We can go on as is for now and debug the Firefox case later.

@bernhl
Copy link

bernhl commented Sep 3, 2020

Did the crash occur on a device with API level 26 (i.e. Android 8.0)? According to https://github.com/google/oboe/blob/master/docs/AndroidAudioHistory.md#80-oreo---api-26-august-21-2017 AAudio is not recommended on 8.0 as it might trigger a crash

@nyorain
Copy link
Contributor Author

nyorain commented Sep 6, 2020

Damn, completely forgot about this. Would still like to help you getting this into cubeb as I'm still using the library.

Is it something that you can send me privately and speed up my testing?

Ok so I wrote up a "minimal" testing example. Minimal in this case still means it has dependencies since setting up logging and message loop and all that on android just requires some code (I also wanted the example to be testable on non-android system for comparison). The code is here. It tests almost all features of cubeb I guess, starting/stopping/draining the stream. When tapping the screen (or clicking the window on desktop systems) it toggles between recording audio and playing back the recorded audio. In my quick testing it behaved the same as the pulseaudio backend (except I'm getting a lot of AAudioStream_read returned not enough frames warnings, does not seem to be a problem but should probably look into it).

I uploaded the apk I tested with here. I have full understanding if you don't want to execute random APKs though. To make it work you would also have to manually enable the 'record audio' permission for the app in android settings (since it's a pure NDK app), otherwise creating the AAudio input stream will fail and the example throw an exception. In case you want to compile it yourself, I'll gladly help with issues. The android-log tag it uses is daudiotest.

Did the crash occur on a device with API level 26 (i.e. Android 8.0)?

Yeah maybe that's a problem, I think I tested it with Android 9 back when I created the PR and with Android 10 today.

@padenot
Copy link
Collaborator

padenot commented Sep 25, 2020

I've imported this into a Firefox build, it works well, I haven't seen crashes, but I'm running Android 9 on a Sony XZ1 Compact (I hear there are issues on Android 8). I should receive a Pixel and a cheaper phone running Android 8 in the next few days.

@nyorain Thanks a lot for this. It's largely working, but some work is needed:

  • Convert to C++, remove all manual lock use (prefer RAII), use smart pointers, generally rely on the type system more, use std::atomic, use std::condvar and lock, etc. This will make the code terser and safer.
  • Add missing functions so that we can have lower latency (better for interactivity, and to make it on par with OpenSL). This probably requires JNI or other ways to get info from the OS. Also add stream type handling to pick the right device (front mic, camera mic, built-in aec/noise suppression/etc.). Roundtrip latency with this backend (that doesn't do anything special yet) is about 135ms (on my Sony XZ1 Compact). It's less than 50ms with our previous backend.
  • Fix or remove todos as appropriate, verify other things, etc. In particular it looks like the state change is now using a regular nanosleep with CLOCK_MONOTONIC here https://android.googlesource.com/platform/frameworks/av/+/refs/heads/master/media/libaaudio/src/utility/AudioClock.h#89.
  • Stress test it, with our usual array of tests: A/V sync with various bluetooth devices, device switch, interruption via notifications, run a lot of streams at once, overload the audio callback thread, compute roundtrip latency, etc.

You've done most of the work, and we need this for Firefox (OpenSL ES is deprecated in NDK r21d, and we'd rather have a bullet-proof AAudio backend sooner than later), I'd be happy to finish this and test on more devices, with more involved test scenarios, etc.

@nyorain
Copy link
Contributor Author

nyorain commented Oct 5, 2020

I'll gladly port it to C++ (am more used to it anyways) and fix the remaining TODOs, will be looking into the clock issue. Should be able to do this until next week.
Regarding the latency functionality and tests however, I'm likely not the right person to get that right since I don't have much experience in that regard (haven't needed it for my usecases of cubeb so far) and am also not exactly eager to do JNI stuff tbh.

@padenot
Copy link
Collaborator

padenot commented Oct 5, 2020

Regarding the latency functionality and tests however, I'm likely not the right person to get that right since I don't have much experience in that regard (haven't needed it for my usecases of cubeb so far) and am also not exactly eager to do JNI stuff tbh.

For sure, it's not particularly exciting. We'll take care of this part, or any part you don't feel like working on, just let us know where and when we can help in any way.

@nyorain
Copy link
Contributor Author

nyorain commented Oct 11, 2020

Rebased and updated the backend to C++11, let me know if it's what you intended.
Regarding the high latency I remembered that I hid the low latency aaudio mode(s) behind config macros that were disabled by default:

One config option specifies whether we pass the low latency flag to the aaudio stream builder (aaudio documentation states that this flag might have an impact on battery life). I enabled that config option by default in cmake now and would consider it sensible to leave it like this but it's up to you what should be enabled by default in cubeb in the end. In my tests, it significantly decreased the output buffer size.

The other flag (that I left disabled but maybe it's useful to you) is to use exclusive sharing mode. The aaudio documentation states that this can reduce the latency further.

This removes the constant checking for state changes with
5ms of sleep in between for the state thread.
We need a new thread to wakeup the state thread reliably
without blocking in the audio thread. For a more detailed
and theoretical explanation of the problem and solution (specifically
written for this commit), see:
https://nyorain.github.io/lock-free-wakeup.html

Now, will only do this time-based sleeping when actively waiting
for a state change. We can't implement that with a blocking call to
AAudioStream_waitForStateChange since that can't be woken up
and we furthermore might have to wait for multiple streams at once.
This also fixes some issues and race conditions with stream destruction
and adds some more documentation.
@padenot
Copy link
Collaborator

padenot commented Oct 12, 2020

Rebased and updated the backend to C++11, let me know if it's what you intended.

From a quick look, this is what I had in mind, thank you very much. I've received test devices, and can test this on Android 8, 9 and 11, now, on a Sony XZ1 Compact, Moto G5 and Pixel 4 devices.

Regarding the high latency I remembered that I hid the low latency aaudio mode(s) behind config macros that were disabled by default:

One config option specifies whether we pass the low latency flag to the aaudio stream builder (aaudio documentation states that this flag might have an impact on battery life). I enabled that config option by default in cmake now and would consider it sensible to leave it like this but it's up to you what should be enabled by default in cubeb in the end. In my tests, it significantly decreased the output buffer size.

cubeb usually makes the decision, per platform, depending on the latency requested, at runtime. This allows having a battery preserving mode for playing regular media (videos, etc.), and a "lowest latency possible in non-exclusive" when doing Web Audio API, WebRTC or other interactive scenarios.

The other flag (that I left disabled but maybe it's useful to you) is to use exclusive sharing mode. The aaudio documentation states that this can reduce the latency further.

We don't use exclusive mode in Firefox, but it might well be useful for others, or for an opt-in mode or something, thanks!

@nyorain
Copy link
Contributor Author

nyorain commented Oct 13, 2020

cubeb usually makes the decision, per platform, depending on the latency requested, at runtime.

Makes sense, could be done here as well. Everything I could do right now is just guesswork though, this should probably be based on queried latencies or general android guarantees. So I would leave it together with the other latency stuff for you.

Let me know how the testing goes, I'll do what I can to fix remaining errors in the code.

// that only serves as notifier backup (in case the notification happens
// right between the state thread checking and going to sleep in which case
// this thread will kick in and signal it right again).
static void notifier_thread(cubeb* ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

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

That should work indeed, the cost of a single thread is acceptable.

It's still unclear to me whether notifying a condition variable is real-time safe on all operating systems that are supported by cubeb. The importance of all this is mitigated by the fact that we only need this when draining.

Copy link
Collaborator

@padenot padenot left a comment

Choose a reason for hiding this comment

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

This is good to merge as is, I'll quickly followup with lots of little things that are needed before it reaches parity with the opensl backend for all usage, at which point we can move it to have a higher precedence than the opensl backend, that will still be used as fallback. I'm also going to have it run the complete audio-related test suite of Firefox, that usually finds lots of edge-cases.

Thank you very much for doing this and then improving it!

@padenot
Copy link
Collaborator

padenot commented Oct 28, 2020

Merging this now, and I'll open another PR (or maybe even multiple PRs) for improvements on latency, picking the correct buffer size, A/V sync, etc.

It is build by default when available, but not used by default (opensl is always preferred except when AAudio is requested and available).

@padenot padenot merged commit 05e2763 into mozilla:master Oct 28, 2020
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.

4 participants