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

Make AudioDriverOpenSL's input callback thread-safe #92969

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

kus04e4ek
Copy link
Contributor

@kus04e4ek kus04e4ek commented Jun 10, 2024

Tested 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 from AudioDriverCoreAudio 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 the AudioDriverCoreAudio 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 the Vector implementation just broke. See #92969 (comment).

platform/android/audio_driver_opensl.cpp Outdated Show resolved Hide resolved
Comment on lines -304 to +314
if (active) {
mutex.lock();
}
output_mutex.lock();
Copy link
Contributor Author

@kus04e4ek kus04e4ek Jun 10, 2024

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

Comment on lines 365 to 366
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()));
Copy link
Contributor Author

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

Comment on lines 98 to 121
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;
}
Copy link
Contributor Author

@kus04e4ek kus04e4ek Jun 11, 2024

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;
Copy link
Contributor Author

@kus04e4ek kus04e4ek Jun 11, 2024

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

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; }
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Comment on lines +60 to +66
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) {}
};
Copy link
Contributor Author

@kus04e4ek kus04e4ek Jun 11, 2024

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 :
Copy link
Contributor Author

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 Show resolved Hide resolved
servers/audio/audio_stream.cpp Outdated Show resolved Hide resolved
Comment on lines 356 to 363
// 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--;
Copy link
Contributor Author

@kus04e4ek kus04e4ek Jun 11, 2024

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #93154

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

Successfully merging this pull request may close these issues.

Audio input gets muted after a while on android
3 participants