-
-
Notifications
You must be signed in to change notification settings - Fork 21.5k
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
Make AudioDriverOpenSL
's input callback thread-safe
#92969
base: master
Are you sure you want to change the base?
Conversation
if (active) { | ||
mutex.lock(); | ||
} | ||
output_mutex.lock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (active)
is not necessary and could create issues(?), no other AudioDriver
checks for it
servers/audio/audio_stream.cpp
Outdated
if (mixed_frames != p_frames) { | ||
ERR_PRINT(vformat("Buffer underrun: input_size = %d, input_ofs = %d, buf.size() = %d.", input_size, input_ofs, buf.size())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was useful for debugging, decided to keep it
bc4749a
to
a69c143
Compare
servers/audio_server.cpp
Outdated
void AudioDriver::input_buffer_init(int driver_buffer_frames) { | ||
const int input_buffer_channels = 2; | ||
input_buffer.resize(driver_buffer_frames * input_buffer_channels * 4); | ||
input_position = 0; | ||
input_size = 0; | ||
input_read = SizePosition(0, 0); | ||
input_write = SizePosition(0, 0); | ||
} | ||
|
||
void AudioDriver::input_buffer_write(int32_t sample) { | ||
if ((int)input_position < input_buffer.size()) { | ||
input_buffer.write[input_position++] = sample; | ||
if ((int)input_position >= input_buffer.size()) { | ||
input_position = 0; | ||
if ((int)input_write.position < input_buffer.size()) { | ||
input_buffer.write[input_write.position++] = sample; | ||
if ((int)input_write.position >= input_buffer.size()) { | ||
input_write.position = 0; | ||
} | ||
if ((int)input_size < input_buffer.size()) { | ||
input_size++; | ||
if ((int)input_write.size < input_buffer.size()) { | ||
input_write.size++; | ||
} | ||
} else { | ||
WARN_PRINT("input_buffer_write: Invalid input_position=" + itos(input_position) + " input_buffer.size()=" + itos(input_buffer.size())); | ||
WARN_PRINT("input_buffer_write: Invalid input_write.position=" + itos(input_write.position) + " input_buffer.size()=" + itos(input_buffer.size())); | ||
} | ||
} | ||
|
||
void AudioDriver::input_buffer_end_write() { | ||
input_read = input_write; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively:
void AudioDriver::input_buffer_init(int driver_buffer_frames) {
const int input_buffer_channels = 2;
input_buffer.resize(driver_buffer_frames * input_buffer_channels * 4);
input_read = SizePosition(0, 0);
}
void AudioDriver::input_buffer_write(int32_t sample) {
SizePosition input_write = input_read;
if ((int)input_write.position < input_buffer.size()) {
input_buffer.write[input_write.position++] = sample;
if ((int)input_write.position >= input_buffer.size()) {
input_write.position = 0;
}
if ((int)input_write.size < input_buffer.size()) {
input_write.size++;
}
input_read = input_write;
} else {
WARN_PRINT("input_buffer_write: Invalid input_write.position=" + itos(input_write.position) + " input_buffer.size()=" + itos(input_buffer.size()));
}
}
Decided to go with this solution, as it seems more performant(?), because of not assigning a value to the std::atomic
multiple times.
unsigned int input_position = 0; | ||
unsigned int input_size = 0; | ||
std::atomic<SizePosition> input_read; | ||
SizePosition input_write; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
input_write
doesn't have to be std::atomic
, mirrors behaviour from the previous implementation
servers/audio_server.h
Outdated
Vector<int32_t> get_input_buffer() { return input_buffer; } | ||
unsigned int get_input_position() { return input_position; } | ||
unsigned int get_input_size() { return input_size; } | ||
const Vector<int32_t> &get_input_buffer() { return input_buffer; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what was causing issues in the first place, for some reason in some rare occasions Vector
was not copied properly, returning Vector
with nullptr
pointer. And just in general there shouldn't be any issues with not copying it, there should be plenty of space in the input buffer
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More about it here: #93125.
struct SizePosition { | ||
unsigned int size; | ||
unsigned int position; | ||
|
||
SizePosition(unsigned int p_size = 0, unsigned int p_position = 0) noexcept : | ||
size(p_size), position(p_position) {} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a whole class to fit it in one std::atomic
, it's important that this values change together. Also it's just a 64-bit structure, so lock-free std::atomic
will probably work on most devices (worked on my phone).
EDIT: It's actually not as important to change them together anymore, mostly for debugging puproses now
unsigned int size; | ||
unsigned int position; | ||
|
||
SizePosition(unsigned int p_size = 0, unsigned int p_position = 0) noexcept : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noexcept
to fix compilation error with g++
servers/audio/audio_stream.cpp
Outdated
// Wait while the other thread writes to the input buffer. | ||
AudioDriver::get_singleton()->input_lock(); | ||
AudioDriver::get_singleton()->input_unlock(); | ||
|
||
size_position = AudioDriver::get_singleton()->get_input_size_position(); | ||
was_locked = true; | ||
|
||
// Write to the same frame on the next loop iteration. | ||
current_frame--; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem that this code actually executes ever, maybe it should be removed and replaced with just printing an error? This would make the third commit fully independent from the first two
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #93154
0e60826
to
7501e75
Compare
AudioDriverOpenSL
's input callback thread-safe #92969 (comment)).AudioStreamPlaybackMicrophone::_mix_internal
be able to run at the same time as input callback forAudioDriverOpenSL
#93154. Still decided to keep it just in caseTested using this project: AudioTest.zip.
Turns out that not making the input callback thread-safe caused weird issues and
input_buffer
was not getting referenced (see #92969 (comment)), so now it's thread-safe. However, the copied implementation fromAudioDriverCoreAudio
causes audio issues (you can hear them if you compile only the first commit), as it seems that input and output callbacks run at the same time on Android. That was a problem because there was only one mutex that these two threads needed to share, but the output callback never waits for this mutex to unlock (to make it as fast as possible, this also matches theAudioDriverCoreAudio
implementation).However, there needed to be at least something to account for them running at the same time, so I decided to add another mutex that output and input callback both wait for. But why doesn't adding another mutex slow down output callback a lot? First of all, it's because this mutex is only used by the input callback, so it doesn't impact speed that much, the other mutex is pretty much used all over Godot and can be used by
GDScript
. Second of all, output callback only waits for the new mutex mid-executing, if it waited for the other mutex, it would have been waiting for it before starting execution. You can look at the implementation in the second commit, that's why there are two commits. This solution also doesn't impact any other audio driver.To minimize locking even more, pushed the third commit. It's also a little bit of a different solution to the issue,
but to function properly it needs the first two commits, that's why I decided to keep it in this PR, can create the new one, if required. Now it locks on the output callback only when there's an underrun in the input buffer. See #92969 (comment) and #93154.The same input mutex can be created for
AudioDriverCoreAudio
as well, but I decided to leave it for the other PR.I could only come up with this solution, so if anyone has other ideas, I would love to hear them ^^
P.S.: There are also other solutions for locks, but I don't know much about threads, is
Mutex
the best lock here or should I use something else? (I'll also ask in Rocket.Chat)EDIT:
This might be a bug that wasn't present since the begging and something in theSee #92969 (comment).Vector
implementation just broke.